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

Commit f177e64

Browse files
committed
add shortcut fields for credential inputs
1 parent ade7f70 commit f177e64

8 files changed

Lines changed: 168 additions & 9 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp
3+
wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5
4+
1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh
5+
3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2
6+
pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX
7+
GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il
8+
AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF
9+
L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k
10+
X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl
11+
U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ
12+
37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0=
13+
-----END RSA PRIVATE KEY-----
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
.. _cli_ref:
2+
3+
Credential Management
4+
=====================
5+
6+
Credential Types and Inputs
7+
-----------------------------
8+
9+
Starting in Ansible Tower 3.2, credential have types defined by a
10+
related table of credential types. Credential types have a name,
11+
kind, and primary key, and can be referenced uniquely by either the
12+
primary key or combination of (name, kind).
13+
14+
Data that the credential contains is embedded in the JSON-type
15+
field ``inputs``. The way to create a credential via the type and
16+
inputs pattern is the following:
17+
18+
::
19+
20+
tower-cli credential create --name="new_cred" --inputs="{username: foo, password: bar}" --credential-type="Machine" --organization="Default"
21+
22+
This method of specifying fields is most congruent with the modern Tower API.
23+
24+
25+
Field Shortcuts
26+
---------------
27+
28+
There are some drawbacks to specifying fields inside of YAML / JSON content
29+
inside of another field. Shortcuts are offered as a way around those.
30+
31+
The most important problem this solves is specifying multi-line input
32+
from a file. This example can be ran from the project root:
33+
34+
::
35+
36+
tower-cli credential modify --name="new_cred" --subinput ssh_key_data @docs/source/cli_ref/examples/data/insecure_private_key
37+
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"
38+
39+
Doing this will put data defined in the file into the `ssh_key_data` key in the
40+
inputs.
41+
42+
The ``--subinput`` option will also perform some conditional type coercion.
43+
At present time, this only matters for boolean type inputs, allowing actions
44+
like the following.
45+
46+
::
47+
48+
tower-cli credential create --name="tower_cred" --inputs="{host: foo.invalid, username: foo, password: bar}" --credential-type="Ansible Tower" --organization=Default
49+
tower-cli credential modify --name=tower_cred --subinput verify_ssl true
50+
51+
In both cases, the point of the ``--subinput`` field is that changing one
52+
field will still perserve the others. For instance, toggling the value of
53+
``verify_ssl`` will not change the value of the ``host`` input.

tower_cli/cli/resource.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -362,6 +362,7 @@ def get_command(self, ctx, name):
362362
type=field.type,
363363
show_default=field.show_default,
364364
multiple=field.multiple,
365+
nargs=field.nargs,
365366
is_eager=False
366367
)(new_method)
367368

tower_cli/models/base.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,9 @@ def _get_patch_url(self, url, pk):
318318
"""Overwrite this method to handle specific corner cases to the url passed to PATCH method."""
319319
return url + '%s/' % pk
320320

321+
def update_from_existing(self, new_data, existing_data):
322+
pass
323+
321324
def write(self, pk=None, create_on_missing=False, fail_on_found=False, force_on_exists=True, **kwargs):
322325
"""
323326
=====API DOCS=====
@@ -385,18 +388,20 @@ def write(self, pk=None, create_on_missing=False, fail_on_found=False, force_on_
385388
answer.update(existing_data)
386389
return answer
387390

391+
# Reinsert None for special case of null association
392+
for key in kwargs:
393+
if kwargs[key] == 'null':
394+
kwargs[key] = None
395+
396+
self.update_from_existing(kwargs, existing_data)
397+
388398
# Similarly, if all existing data matches our write parameters, there's no need to do anything.
389399
if all([kwargs[k] == existing_data.get(k, None) for k in kwargs.keys()]):
390400
debug.log('All provided fields match existing data; do nothing.', header='decision', nl=2)
391401
answer = OrderedDict((('changed', False), ('id', pk)))
392402
answer.update(existing_data)
393403
return answer
394404

395-
# Reinsert None for special case of null association
396-
for key in kwargs:
397-
if kwargs[key] == 'null':
398-
kwargs[key] = None
399-
400405
# Get the URL and method to use for the write.
401406
url = self.endpoint
402407
method = 'POST'

tower_cli/models/fields.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ def __init__(self, key=None, type=six.text_type, default=None,
4545
display=True, filterable=True, help_text=None,
4646
is_option=True, password=False, read_only=False,
4747
required=True, show_default=False, unique=False,
48-
multiple=False, no_lookup=False, col_width=None):
48+
multiple=False, nargs=1, no_lookup=False, col_width=None):
4949
# Init the name to blank.
5050
# What's going on here: This is set by the ResourceMeta metaclass
5151
# when the **resource** is instantiated.
@@ -67,6 +67,7 @@ def __init__(self, key=None, type=six.text_type, default=None,
6767
self.show_default = show_default
6868
self.unique = unique
6969
self.multiple = multiple
70+
self.nargs = nargs
7071
self.no_lookup = no_lookup
7172
self.col_width = col_width
7273

tower_cli/resources/credential.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515

16-
from tower_cli import models
16+
import six
17+
import os
18+
19+
from tower_cli import models, exceptions as exc, get_resource
1720
from tower_cli.cli import types
21+
from tower_cli.utils import debug, str_to_bool
1822

1923

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

39+
# Core functionality
3540
credential_type = models.Field(type=types.Related('credential_type'))
3641
inputs = models.Field(type=types.StructuredInput(), required=False, display=False)
42+
43+
# Fields for reverse compatibility
44+
subinput = models.Field(
45+
required=False, nargs=2, multiple=True, display=False,
46+
help_text='A key and value to be combined into the credential inputs JSON data.'
47+
' Start the value with "@" to obtain from a file.\n'
48+
'Example: `--subinput ssh_key_data @filename` would apply an SSH private key'
49+
)
50+
51+
def update_from_existing(self, kwargs, existing_data):
52+
subinputs = kwargs.pop('subinput', ())
53+
if not subinputs:
54+
return super(Resource, self).update_from_existing(kwargs, existing_data)
55+
56+
inputs = {}
57+
if kwargs.get('inputs'):
58+
inputs = kwargs['inputs'].copy()
59+
elif existing_data.get('inputs'):
60+
inputs = existing_data['inputs'].copy()
61+
62+
ct_pk = None
63+
if existing_data:
64+
ct_pk = existing_data.get('credential_type')
65+
else:
66+
ct_pk = kwargs.get('credential_type')
67+
if not ct_pk:
68+
debug.log('Could not apply subinputs because of unknown credential type')
69+
return super(Resource, self).update_from_existing(kwargs, existing_data)
70+
71+
ct_res = get_resource('credential_type')
72+
schema = ct_res.get(ct_pk)['inputs'].get('fields', [])
73+
schema_map = {}
74+
for element in schema:
75+
schema_map[element.get('id', '')] = element
76+
77+
for key, raw_value in subinputs:
78+
if kwargs and kwargs.get(key, {}).get(key):
79+
raise exc.BadRequest(
80+
'Field {} specified in both --subinput and --inputs.'.format(key)
81+
)
82+
83+
# Read from a file if starts with "@"
84+
if raw_value.startswith('@'):
85+
filename = os.path.expanduser(raw_value[1:])
86+
with open(filename, 'r') as f:
87+
value = f.read()
88+
else:
89+
value = raw_value
90+
91+
# Type conversion
92+
if key not in schema_map:
93+
debug.log('Credential type inputs:\n{}'.format(schema))
94+
raise exc.BadRequest(
95+
'Field {} is not allowed by credential type inputs.'.format(key)
96+
)
97+
type_str = schema_map[key].get('type', 'string')
98+
99+
converters = {
100+
'string': six.text_type,
101+
'boolean': str_to_bool
102+
}
103+
if type_str not in converters:
104+
raise exc.BadRequest(
105+
'Credential type {} input {} uses an unrecognized type: {}.'.format(
106+
ct_pk, key, type_str)
107+
)
108+
converter = converters[type_str]
109+
try:
110+
value = converter(value)
111+
except Exception as e:
112+
raise exc.BadRequest(
113+
'Field {} in --subinput is not type {} specified by credential type '
114+
'{} inputs.\n(error: {})'.format(key, type_str, ct_pk, e)
115+
)
116+
inputs[key] = value
117+
kwargs['inputs'] = inputs

tower_cli/resources/setting.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
import ast
1616
import json
17-
from distutils.util import strtobool
1817

1918
import click
2019
import six
@@ -23,6 +22,7 @@
2322
from tower_cli.api import client
2423
from tower_cli.conf import pop_option
2524
from tower_cli.cli import types
25+
from tower_cli.utils import str_to_bool
2626
from tower_cli.utils.data_structures import OrderedDict
2727

2828

@@ -153,7 +153,7 @@ def coerce_type(self, key, value):
153153
if to_type == 'integer':
154154
return int(value)
155155
elif to_type == 'boolean':
156-
return bool(strtobool(value))
156+
return str_to_bool(value)
157157
elif to_type in ('list', 'nested object'):
158158
return ast.literal_eval(value)
159159
return value

tower_cli/utils/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
# limitations under the License.
1515

1616
import functools
17+
from distutils.util import strtobool
1718

1819
import click
1920

@@ -44,3 +45,7 @@ def supports_oauth():
4445
except exceptions.NotFound:
4546
return False
4647
return resp.ok
48+
49+
50+
def str_to_bool(value):
51+
return bool(strtobool(value))

0 commit comments

Comments
 (0)