mirror of
https://github.com/ansible/awx.git
synced 2026-05-09 18:37:36 -02:30
Add support for credential_type in tower_credential module (#3820)
* Add support for credential_type * Finish up credential_type parameter with tests * make inputs mutually exclusive with other params * Test credential type with dict input
This commit is contained in:
@@ -34,7 +34,7 @@ in the `awx_collection_test_venv` folder so that `make test_collection` can
|
|||||||
be ran to actually run the tests. A single test can be ran via:
|
be ran to actually run the tests. A single test can be ran via:
|
||||||
|
|
||||||
```
|
```
|
||||||
make test_collection MODULE_TEST_DIRS=awx_collection/test/awx/test_organization.py
|
make test_collection COLLECTION_TEST_DIRS=awx_collection/test/awx/test_organization.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|||||||
@@ -98,8 +98,8 @@ class TowerModule(AnsibleModule):
|
|||||||
)
|
)
|
||||||
args.update(argument_spec)
|
args.update(argument_spec)
|
||||||
|
|
||||||
mutually_exclusive = kwargs.get('mutually_exclusive', [])
|
kwargs.setdefault('mutually_exclusive', [])
|
||||||
kwargs['mutually_exclusive'] = mutually_exclusive.extend((
|
kwargs['mutually_exclusive'].extend((
|
||||||
('tower_config_file', 'tower_host'),
|
('tower_config_file', 'tower_host'),
|
||||||
('tower_config_file', 'tower_username'),
|
('tower_config_file', 'tower_username'),
|
||||||
('tower_config_file', 'tower_password'),
|
('tower_config_file', 'tower_password'),
|
||||||
|
|||||||
@@ -53,9 +53,23 @@ options:
|
|||||||
description:
|
description:
|
||||||
- Type of credential being added.
|
- Type of credential being added.
|
||||||
- The ssh choice refers to a Tower Machine credential.
|
- The ssh choice refers to a Tower Machine credential.
|
||||||
required: True
|
required: False
|
||||||
type: str
|
type: str
|
||||||
choices: ["ssh", "vault", "net", "scm", "aws", "vmware", "satellite6", "cloudforms", "gce", "azure_rm", "openstack", "rhv", "insights", "tower"]
|
choices: ["ssh", "vault", "net", "scm", "aws", "vmware", "satellite6", "cloudforms", "gce", "azure_rm", "openstack", "rhv", "insights", "tower"]
|
||||||
|
credential_type:
|
||||||
|
description:
|
||||||
|
- Name of credential type.
|
||||||
|
required: False
|
||||||
|
version_added: "2.10"
|
||||||
|
type: str
|
||||||
|
inputs:
|
||||||
|
description:
|
||||||
|
- >-
|
||||||
|
Credential inputs where the keys are var names used in templating.
|
||||||
|
Refer to the Ansible Tower documentation for example syntax.
|
||||||
|
required: False
|
||||||
|
version_added: "2.9"
|
||||||
|
type: dict
|
||||||
host:
|
host:
|
||||||
description:
|
description:
|
||||||
- Host for this credential.
|
- Host for this credential.
|
||||||
@@ -185,6 +199,15 @@ EXAMPLES = '''
|
|||||||
tower_host: https://localhost
|
tower_host: https://localhost
|
||||||
run_once: true
|
run_once: true
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
|
- name: Add Credential with Custom Credential Type
|
||||||
|
tower_credential:
|
||||||
|
name: Workshop Credential
|
||||||
|
credential_type: MyCloudCredential
|
||||||
|
organization: Default
|
||||||
|
tower_username: admin
|
||||||
|
tower_password: ansible
|
||||||
|
tower_host: https://localhost
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import os
|
import os
|
||||||
@@ -219,7 +242,17 @@ KIND_CHOICES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def credential_type_for_v1_kind(params, module):
|
OLD_INPUT_NAMES = (
|
||||||
|
'authorize', 'authorize_password', 'client',
|
||||||
|
'security_token', 'secret', 'tenant', 'subscription',
|
||||||
|
'domain', 'become_method', 'become_username',
|
||||||
|
'become_password', 'vault_password', 'project', 'host',
|
||||||
|
'username', 'password', 'ssh_key_data', 'vault_id',
|
||||||
|
'ssh_key_unlock'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def credential_type_for_kind(params):
|
||||||
credential_type_res = tower_cli.get_resource('credential_type')
|
credential_type_res = tower_cli.get_resource('credential_type')
|
||||||
kind = params.pop('kind')
|
kind = params.pop('kind')
|
||||||
arguments = {'managed_by_tower': True}
|
arguments = {'managed_by_tower': True}
|
||||||
@@ -244,8 +277,9 @@ def main():
|
|||||||
name=dict(required=True),
|
name=dict(required=True),
|
||||||
user=dict(),
|
user=dict(),
|
||||||
team=dict(),
|
team=dict(),
|
||||||
kind=dict(required=True,
|
kind=dict(choices=KIND_CHOICES.keys()),
|
||||||
choices=KIND_CHOICES.keys()),
|
credential_type=dict(),
|
||||||
|
inputs=dict(type='dict'),
|
||||||
host=dict(),
|
host=dict(),
|
||||||
username=dict(),
|
username=dict(),
|
||||||
password=dict(no_log=True),
|
password=dict(no_log=True),
|
||||||
@@ -270,7 +304,14 @@ def main():
|
|||||||
vault_id=dict(),
|
vault_id=dict(),
|
||||||
)
|
)
|
||||||
|
|
||||||
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True)
|
mutually_exclusive = [
|
||||||
|
('kind', 'credential_type')
|
||||||
|
]
|
||||||
|
for input_name in OLD_INPUT_NAMES:
|
||||||
|
mutually_exclusive.append(('inputs', input_name))
|
||||||
|
|
||||||
|
module = TowerModule(argument_spec=argument_spec, supports_check_mode=True,
|
||||||
|
mutually_exclusive=mutually_exclusive)
|
||||||
|
|
||||||
name = module.params.get('name')
|
name = module.params.get('name')
|
||||||
organization = module.params.get('organization')
|
organization = module.params.get('organization')
|
||||||
@@ -298,10 +339,26 @@ def main():
|
|||||||
# /api/v1/ backwards compat
|
# /api/v1/ backwards compat
|
||||||
# older versions of tower-cli don't *have* a credential_type
|
# older versions of tower-cli don't *have* a credential_type
|
||||||
# resource
|
# resource
|
||||||
params['kind'] = module.params['kind']
|
params['kind'] = module.params.get('kind')
|
||||||
else:
|
else:
|
||||||
credential_type = credential_type_for_v1_kind(module.params, module)
|
if module.params.get('credential_type'):
|
||||||
params['credential_type'] = credential_type['id']
|
credential_type_res = tower_cli.get_resource('credential_type')
|
||||||
|
try:
|
||||||
|
credential_type = credential_type_res.get(name=module.params['credential_type'])
|
||||||
|
except (exc.NotFound) as excinfo:
|
||||||
|
module.fail_json(msg=(
|
||||||
|
'Failed to update credential, credential_type not found: {0}'
|
||||||
|
).format(excinfo), changed=False)
|
||||||
|
params['credential_type'] = credential_type['id']
|
||||||
|
|
||||||
|
if module.params.get('inputs'):
|
||||||
|
params['inputs'] = module.params.get('inputs')
|
||||||
|
|
||||||
|
elif module.params.get('kind'):
|
||||||
|
credential_type = credential_type_for_kind(module.params)
|
||||||
|
params['credential_type'] = credential_type['id']
|
||||||
|
else:
|
||||||
|
module.fail_json(msg='must either specify credential_type or kind', changed=False)
|
||||||
|
|
||||||
if module.params.get('description'):
|
if module.params.get('description'):
|
||||||
params['description'] = module.params.get('description')
|
params['description'] = module.params.get('description')
|
||||||
@@ -333,12 +390,7 @@ def main():
|
|||||||
if module.params.get('vault_id', None) and module.params.get('kind') != 'vault':
|
if module.params.get('vault_id', None) and module.params.get('kind') != 'vault':
|
||||||
module.fail_json(msg="Parameter 'vault_id' is only valid if parameter 'kind' is specified as 'vault'")
|
module.fail_json(msg="Parameter 'vault_id' is only valid if parameter 'kind' is specified as 'vault'")
|
||||||
|
|
||||||
for key in ('authorize', 'authorize_password', 'client',
|
for key in OLD_INPUT_NAMES:
|
||||||
'security_token', 'secret', 'tenant', 'subscription',
|
|
||||||
'domain', 'become_method', 'become_username',
|
|
||||||
'become_password', 'vault_password', 'project', 'host',
|
|
||||||
'username', 'password', 'ssh_key_data', 'vault_id',
|
|
||||||
'ssh_key_unlock'):
|
|
||||||
if 'kind' in params:
|
if 'kind' in params:
|
||||||
params[key] = module.params.get(key)
|
params[key] = module.params.get(key)
|
||||||
elif module.params.get(key):
|
elif module.params.get(key):
|
||||||
|
|||||||
@@ -62,6 +62,9 @@ def run_module():
|
|||||||
# We should consider supporting that in the future
|
# We should consider supporting that in the future
|
||||||
resource_module = importlib.import_module('plugins.modules.{}'.format(module_name))
|
resource_module = importlib.import_module('plugins.modules.{}'.format(module_name))
|
||||||
|
|
||||||
|
if not isinstance(module_params, dict):
|
||||||
|
raise RuntimeError('Module params must be dict, got {}'.format(type(module_params)))
|
||||||
|
|
||||||
# Ansible params can be passed as an invocation argument or over stdin
|
# Ansible params can be passed as an invocation argument or over stdin
|
||||||
# this short circuits within the AnsibleModule interface
|
# this short circuits within the AnsibleModule interface
|
||||||
def mock_load_params(self):
|
def mock_load_params(self):
|
||||||
|
|||||||
151
awx_collection/test/awx/test_credential.py
Normal file
151
awx_collection/test/awx/test_credential.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from awx.main.models import Credential, CredentialType, Organization
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_machine_credential(run_module, admin_user):
|
||||||
|
Organization.objects.create(name='test-org')
|
||||||
|
# create the ssh credential type
|
||||||
|
CredentialType.defaults['ssh']().save()
|
||||||
|
# Example from docs
|
||||||
|
result = run_module('tower_credential', dict(
|
||||||
|
name='Team Name',
|
||||||
|
description='Team Description',
|
||||||
|
organization='test-org',
|
||||||
|
kind='ssh',
|
||||||
|
state='present'
|
||||||
|
), admin_user)
|
||||||
|
|
||||||
|
cred = Credential.objects.get(name='Team Name')
|
||||||
|
result.pop('invocation')
|
||||||
|
assert result == {
|
||||||
|
"credential": "Team Name",
|
||||||
|
"state": "present",
|
||||||
|
"id": cred.pk,
|
||||||
|
"changed": True
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_custom_credential_type(run_module, admin_user):
|
||||||
|
# Example from docs
|
||||||
|
result = run_module('tower_credential_type', dict(
|
||||||
|
name='Nexus',
|
||||||
|
description='Credentials type for Nexus',
|
||||||
|
kind='cloud',
|
||||||
|
inputs={"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []},
|
||||||
|
injectors={'extra_vars': {'nexus_credential': 'test'}},
|
||||||
|
state='present',
|
||||||
|
validate_certs='false'
|
||||||
|
), admin_user)
|
||||||
|
|
||||||
|
ct = CredentialType.objects.get(name='Nexus')
|
||||||
|
result.pop('invocation')
|
||||||
|
assert result == {
|
||||||
|
"credential_type": "Nexus",
|
||||||
|
"state": "present",
|
||||||
|
"id": ct.pk,
|
||||||
|
"changed": True
|
||||||
|
}
|
||||||
|
|
||||||
|
assert ct.inputs == {"fields": [{"id": "server", "type": "string", "default": "", "label": ""}], "required": []}
|
||||||
|
assert ct.injectors == {'extra_vars': {'nexus_credential': 'test'}}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_kind_ct_exclusivity(run_module, admin_user):
|
||||||
|
result = run_module('tower_credential', dict(
|
||||||
|
name='A credential',
|
||||||
|
organization='test-org',
|
||||||
|
kind='ssh',
|
||||||
|
credential_type='foobar', # cannot specify if kind is also specified
|
||||||
|
state='present'
|
||||||
|
), admin_user)
|
||||||
|
|
||||||
|
result.pop('invocation')
|
||||||
|
assert result == {
|
||||||
|
'failed': True,
|
||||||
|
'msg': 'parameters are mutually exclusive: kind|credential_type'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_input_exclusivity(run_module, admin_user):
|
||||||
|
result = run_module('tower_credential', dict(
|
||||||
|
name='A credential',
|
||||||
|
organization='test-org',
|
||||||
|
kind='ssh',
|
||||||
|
inputs={'token': '7rEZK38DJl58A7RxA6EC7lLvUHbBQ1'},
|
||||||
|
security_token='7rEZK38DJl58A7RxA6EC7lLvUHbBQ1',
|
||||||
|
state='present'
|
||||||
|
), admin_user)
|
||||||
|
|
||||||
|
result.pop('invocation')
|
||||||
|
assert result == {
|
||||||
|
'failed': True,
|
||||||
|
'msg': 'parameters are mutually exclusive: inputs|security_token'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_missing_credential_type(run_module, admin_user):
|
||||||
|
Organization.objects.create(name='test-org')
|
||||||
|
result = run_module('tower_credential', dict(
|
||||||
|
name='A credential',
|
||||||
|
organization='test-org',
|
||||||
|
credential_type='foobar',
|
||||||
|
state='present'
|
||||||
|
), admin_user)
|
||||||
|
|
||||||
|
result.pop('invocation')
|
||||||
|
assert result == {
|
||||||
|
"changed": False,
|
||||||
|
"failed": True,
|
||||||
|
'msg': 'Failed to update credential, credential_type not found: The requested object could not be found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_make_use_of_custom_credential_type(run_module, admin_user):
|
||||||
|
Organization.objects.create(name='test-org')
|
||||||
|
# Make a credential type which will be used by the credential
|
||||||
|
ct = CredentialType.objects.create(
|
||||||
|
name='Ansible Galaxy Token',
|
||||||
|
inputs={
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"id": "token",
|
||||||
|
"type": "string",
|
||||||
|
"secret": True,
|
||||||
|
"label": "Ansible Galaxy Secret Token Value"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"required": ["token"]
|
||||||
|
},
|
||||||
|
injectors={
|
||||||
|
"extra_vars": {
|
||||||
|
"galaxy_token": "{{token}}",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
result = run_module('tower_credential', dict(
|
||||||
|
name='Galaxy Token for Steve',
|
||||||
|
organization='test-org',
|
||||||
|
credential_type='Ansible Galaxy Token',
|
||||||
|
inputs={'token': '7rEZK38DJl58A7RxA6EC7lLvUHbBQ1'},
|
||||||
|
state='present'
|
||||||
|
), admin_user)
|
||||||
|
|
||||||
|
cred = Credential.objects.get(name='Galaxy Token for Steve')
|
||||||
|
assert cred.credential_type_id == ct.id
|
||||||
|
assert list(cred.inputs.keys()) == ['token']
|
||||||
|
assert cred.inputs['token'].startswith('$encrypted$')
|
||||||
|
assert len(cred.inputs['token']) >= len('$encrypted$') + len('7rEZK38DJl58A7RxA6EC7lLvUHbBQ1')
|
||||||
|
result.pop('invocation')
|
||||||
|
assert result == {
|
||||||
|
"credential": "Galaxy Token for Steve",
|
||||||
|
"state": "present",
|
||||||
|
"id": cred.pk,
|
||||||
|
"changed": True
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user