Skip to content
This repository has been archived by the owner on Sep 16, 2020. It is now read-only.

Commit

Permalink
add shortcut fields for credential inputs
Browse files Browse the repository at this point in the history
  • Loading branch information
AlanCoding committed Jun 15, 2018
1 parent ade7f70 commit 07e4ba6
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 11 deletions.
13 changes: 13 additions & 0 deletions docs/source/cli_ref/examples/data/insecure_private_key
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp
wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5
1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh
3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2
pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX
GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il
AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF
L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k
X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl
U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ
37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0=
-----END RSA PRIVATE KEY-----
53 changes: 53 additions & 0 deletions docs/source/cli_ref/usage/CREDENTIALS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
.. _cli_ref:

Credential Management
=====================

Credential Types and Inputs
-----------------------------

Starting in Ansible Tower 3.2, credential have types defined by a
related table of credential types. Credential types have a name,
kind, and primary key, and can be referenced uniquely by either the
primary key or combination of (name, kind).

Data that the credential contains is embedded in the JSON-type
field ``inputs``. The way to create a credential via the type and
inputs pattern is the following:

::

tower-cli credential create --name="new_cred" --inputs="{username: foo, password: bar}" --credential-type="Machine" --organization="Default"

This method of specifying fields is most congruent with the modern Tower API.


Field Shortcuts
---------------

There are some drawbacks to specifying fields inside of YAML / JSON content
inside of another field. Shortcuts are offered as a way around those.

The most important problem this solves is specifying multi-line input
from a file. This example can be ran from the project root:

::

tower-cli credential modify --name="new_cred" --subinput ssh_key_data @docs/source/cli_ref/examples/data/insecure_private_key
tower-cli credential create --name="only_ssh_key" --subinput ssh_key_data @docs/source/cli_ref/examples/data/insecure_private_key --credential-type="Machine" --organization="Default"

Doing this will put data defined in the file into the `ssh_key_data` key in the
inputs.

The ``--subinput`` option will also perform some conditional type coercion.
At present time, this only matters for boolean type inputs, allowing actions
like the following.

::

tower-cli credential create --name="tower_cred" --inputs="{host: foo.invalid, username: foo, password: bar}" --credential-type="Ansible Tower" --organization=Default
tower-cli credential modify --name=tower_cred --subinput verify_ssl true

In both cases, the point of the ``--subinput`` field is that changing one
field will still perserve the others. For instance, toggling the value of
``verify_ssl`` will not change the value of the ``host`` input.
5 changes: 3 additions & 2 deletions tests/test_models_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,11 +471,12 @@ def test_write_file_like_object(self):
def test_write_with_null_field(self):
"""Establish that a resource with 'null' field is written."""
with client.test_mode as t:
t.register_json('/foo/42/', {'id': 42, 'name': 'bar',
t.register_json('/foo/42/', {'id': 42, 'name': 'bar', 'inventory': 49,
'description': 'baz'}, method='GET')
t.register_json('/foo/42/', {'name': 'bar', 'id': 42,
'inventory': 'null'}, method='PATCH')
self.res.write(42, inventory='null')
r = self.res.write(42, inventory='null')
assert r['changed']
self.assertEqual(json.loads(t.requests[1].body)['inventory'], None)

def test_delete_with_pk(self):
Expand Down
1 change: 1 addition & 0 deletions tower_cli/cli/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ def get_command(self, ctx, name):
type=field.type,
show_default=field.show_default,
multiple=field.multiple,
nargs=field.nargs,
is_eager=False
)(new_method)

Expand Down
15 changes: 10 additions & 5 deletions tower_cli/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,9 @@ def _get_patch_url(self, url, pk):
"""Overwrite this method to handle specific corner cases to the url passed to PATCH method."""
return url + '%s/' % pk

def update_from_existing(self, new_data, existing_data):
pass

def write(self, pk=None, create_on_missing=False, fail_on_found=False, force_on_exists=True, **kwargs):
"""
=====API DOCS=====
Expand Down Expand Up @@ -385,18 +388,20 @@ def write(self, pk=None, create_on_missing=False, fail_on_found=False, force_on_
answer.update(existing_data)
return answer

# Reinsert None for special case of null association
for key in kwargs:
if kwargs[key] == 'null':
kwargs[key] = None

self.update_from_existing(kwargs, existing_data)

# Similarly, if all existing data matches our write parameters, there's no need to do anything.
if all([kwargs[k] == existing_data.get(k, None) for k in kwargs.keys()]):
debug.log('All provided fields match existing data; do nothing.', header='decision', nl=2)
answer = OrderedDict((('changed', False), ('id', pk)))
answer.update(existing_data)
return answer

# Reinsert None for special case of null association
for key in kwargs:
if kwargs[key] == 'null':
kwargs[key] = None

# Get the URL and method to use for the write.
url = self.endpoint
method = 'POST'
Expand Down
3 changes: 2 additions & 1 deletion tower_cli/models/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def __init__(self, key=None, type=six.text_type, default=None,
display=True, filterable=True, help_text=None,
is_option=True, password=False, read_only=False,
required=True, show_default=False, unique=False,
multiple=False, no_lookup=False, col_width=None):
multiple=False, nargs=1, no_lookup=False, col_width=None):
# Init the name to blank.
# What's going on here: This is set by the ResourceMeta metaclass
# when the **resource** is instantiated.
Expand All @@ -67,6 +67,7 @@ def __init__(self, key=None, type=six.text_type, default=None,
self.show_default = show_default
self.unique = unique
self.multiple = multiple
self.nargs = nargs
self.no_lookup = no_lookup
self.col_width = col_width

Expand Down
83 changes: 82 additions & 1 deletion tower_cli/resources/credential.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from tower_cli import models
import six
import os

from tower_cli import models, exceptions as exc, get_resource
from tower_cli.cli import types
from tower_cli.utils import debug, str_to_bool


class Resource(models.Resource):
Expand All @@ -32,5 +36,82 @@ class Resource(models.Resource):
team = models.Field(display=False, type=types.Related('team'), required=False, no_lookup=True)
organization = models.Field(display=False, type=types.Related('organization'), required=False)

# Core functionality
credential_type = models.Field(type=types.Related('credential_type'))
inputs = models.Field(type=types.StructuredInput(), required=False, display=False)

# Fields for reverse compatibility
subinput = models.Field(
required=False, nargs=2, multiple=True, display=False,
help_text='A key and value to be combined into the credential inputs JSON data.'
' Start the value with "@" to obtain from a file.\n'
'Example: `--subinput ssh_key_data @filename` would apply an SSH private key'
)

def update_from_existing(self, kwargs, existing_data):
subinputs = kwargs.pop('subinput', ())
if not subinputs:
return super(Resource, self).update_from_existing(kwargs, existing_data)

inputs = {}
if kwargs.get('inputs'):
inputs = kwargs['inputs'].copy()
elif existing_data.get('inputs'):
inputs = existing_data['inputs'].copy()

ct_pk = None
if existing_data:
ct_pk = existing_data.get('credential_type')
else:
ct_pk = kwargs.get('credential_type')
if not ct_pk:
debug.log('Could not apply subinputs because of unknown credential type')
return super(Resource, self).update_from_existing(kwargs, existing_data)

ct_res = get_resource('credential_type')
schema = ct_res.get(ct_pk)['inputs'].get('fields', [])
schema_map = {}
for element in schema:
schema_map[element.get('id', '')] = element

for key, raw_value in subinputs:
if kwargs and kwargs.get(key, {}).get(key):
raise exc.BadRequest(
'Field {} specified in both --subinput and --inputs.'.format(key)
)

# Read from a file if starts with "@"
if raw_value.startswith('@'):
filename = os.path.expanduser(raw_value[1:])
with open(filename, 'r') as f:
value = f.read()
else:
value = raw_value

# Type conversion
if key not in schema_map:
debug.log('Credential type inputs:\n{}'.format(schema))
raise exc.BadRequest(
'Field {} is not allowed by credential type inputs.'.format(key)
)
type_str = schema_map[key].get('type', 'string')

converters = {
'string': six.text_type,
'boolean': str_to_bool
}
if type_str not in converters:
raise exc.BadRequest(
'Credential type {} input {} uses an unrecognized type: {}.'.format(
ct_pk, key, type_str)
)
converter = converters[type_str]
try:
value = converter(value)
except Exception as e:
raise exc.BadRequest(
'Field {} in --subinput is not type {} specified by credential type '
'{} inputs.\n(error: {})'.format(key, type_str, ct_pk, e)
)
inputs[key] = value
kwargs['inputs'] = inputs
4 changes: 2 additions & 2 deletions tower_cli/resources/setting.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

import ast
import json
from distutils.util import strtobool

import click
import six
Expand All @@ -23,6 +22,7 @@
from tower_cli.api import client
from tower_cli.conf import pop_option
from tower_cli.cli import types
from tower_cli.utils import str_to_bool
from tower_cli.utils.data_structures import OrderedDict


Expand Down Expand Up @@ -153,7 +153,7 @@ def coerce_type(self, key, value):
if to_type == 'integer':
return int(value)
elif to_type == 'boolean':
return bool(strtobool(value))
return str_to_bool(value)
elif to_type in ('list', 'nested object'):
return ast.literal_eval(value)
return value
Expand Down
5 changes: 5 additions & 0 deletions tower_cli/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# limitations under the License.

import functools
from distutils.util import strtobool

import click

Expand Down Expand Up @@ -44,3 +45,7 @@ def supports_oauth():
except exceptions.NotFound:
return False
return resp.ok


def str_to_bool(value):
return bool(strtobool(value))

0 comments on commit 07e4ba6

Please sign in to comment.