Merge pull request #6495 from john-westcott-iv/tower_credential_update_new

Initial conversion of tower_credential

Reviewed-by: https://github.com/apps/softwarefactory-project-zuul
This commit is contained in:
softwarefactory-project-zuul[bot]
2020-04-06 20:29:44 +00:00
committed by GitHub
5 changed files with 509 additions and 349 deletions

View File

@@ -52,6 +52,7 @@ The following notes are changes that may require changes to playbooks:
- Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended. - Some return values (e.g., `credential_type`) have been removed. Use of `id` is recommended.
- `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality. - `tower_job_template` no longer supports the deprecated `extra_vars_path` parameter, please use `extra_vars` with the lookup plugin to replace this functionality.
- The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict. - The `notification_configuration` parameter of `tower_notification` has changed from a string to a dict. Please use the `lookup` plugin to read an existing file into a dict.
- `tower_credential` no longer supports passing a file name to ssh_key_data.
## Running Unit Tests ## Running Unit Tests

View File

@@ -265,7 +265,14 @@ class TowerModule(AnsibleModule):
def resolve_name_to_id(self, endpoint, name_or_id): def resolve_name_to_id(self, endpoint, name_or_id):
# Try to resolve the object by name # Try to resolve the object by name
response = self.get_endpoint(endpoint, **{'data': {'name': name_or_id}}) name_field = 'name'
if endpoint == 'users':
name_field = 'username'
response = self.get_endpoint(endpoint, **{'data': {name_field: name_or_id}})
if response['status_code'] == 400:
self.fail_json(msg="Unable to try and resolve {0} for {1} : {2}".format(endpoint, name_or_id, response['json']['detail']))
if response['json']['count'] == 1: if response['json']['count'] == 1:
return response['json']['results'][0]['id'] return response['json']['results'][0]['id']
elif response['json']['count'] == 0: elif response['json']['count'] == 0:

View File

@@ -28,37 +28,24 @@ options:
- The name to use for the credential. - The name to use for the credential.
required: True required: True
type: str type: str
new_name:
description:
- Setting this option will change the existing name (looked up via the name field.
required: False
type: str
description: description:
description: description:
- The description to use for the credential. - The description to use for the credential.
type: str type: str
user:
description:
- User that should own this credential.
type: str
team:
description:
- Team that should own this credential.
type: str
project:
description:
- Project that should use this credential.
type: str
organization: organization:
description: description:
- Organization that should own the credential. - Organization that should own the credential.
required: True
type: str
kind:
description:
- Type of credential being added.
- The ssh choice refers to a Tower Machine credential.
required: False required: False
type: str type: str
choices: ["ssh", "vault", "net", "scm", "aws", "vmware", "satellite6", "cloudforms", "gce", "azure_rm", "openstack", "rhv", "insights", "tower"]
credential_type: credential_type:
description: description:
- Name of credential type. - Name of credential type.
- Will be preferred over kind
required: False required: False
version_added: "2.10" version_added: "2.10"
type: str type: str
@@ -67,91 +54,132 @@ options:
- >- - >-
Credential inputs where the keys are var names used in templating. Credential inputs where the keys are var names used in templating.
Refer to the Ansible Tower documentation for example syntax. Refer to the Ansible Tower documentation for example syntax.
- Any fields in this dict will take prescedence over any fields mentioned below (i.e. host, username, etc)
required: False required: False
version_added: "2.9" version_added: "2.9"
type: dict type: dict
user:
description:
- User that should own this credential.
type: str
team:
description:
- Team that should own this credential.
type: str
kind:
description:
- Type of credential being added.
- The ssh choice refers to a Tower Machine credential.
- Deprecated, please use credential_type
required: False
type: str
choices: ["ssh", "vault", "net", "scm", "aws", "vmware", "satellite6", "cloudforms", "gce", "azure_rm", "openstack", "rhv", "insights", "tower"]
host: host:
description: description:
- Host for this credential. - Host for this credential.
- Deprecated, will be removed in a future release
type: str type: str
username: username:
description: description:
- Username for this credential. ``access_key`` for AWS. - Username for this credential. ``access_key`` for AWS.
- Deprecated, please use inputs
type: str type: str
password: password:
description: description:
- Password for this credential. ``secret_key`` for AWS. ``api_key`` for RAX. - Password for this credential. ``secret_key`` for AWS. ``api_key`` for RAX.
- Use "ASK" and launch in Tower to be prompted. - Use "ASK" and launch in Tower to be prompted.
- Deprecated, please use inputs
type: str
project:
description:
- Project that should use this credential for GCP.
- Deprecated, will be removed in a future release
type: str type: str
ssh_key_data: ssh_key_data:
description: description:
- SSH private key content. To extract the content from a file path, use the lookup function (see examples). - SSH private key content. To extract the content from a file path, use the lookup function (see examples).
- Deprecated, please use inputs
required: False required: False
type: str type: str
ssh_key_unlock: ssh_key_unlock:
description: description:
- Unlock password for ssh_key. - Unlock password for ssh_key.
- Use "ASK" and launch in Tower to be prompted. - Use "ASK" and launch in Tower to be prompted.
- Deprecated, please use inputs
type: str type: str
authorize: authorize:
description: description:
- Should use authorize for net type. - Should use authorize for net type.
- Deprecated, please use inputs
type: bool type: bool
default: 'no' default: 'no'
authorize_password: authorize_password:
description: description:
- Password for net credentials that require authorize. - Password for net credentials that require authorize.
- Deprecated, please use inputs
type: str type: str
client: client:
description: description:
- Client or application ID for azure_rm type. - Client or application ID for azure_rm type.
- Deprecated, please use inputs
type: str type: str
security_token: security_token:
description: description:
- STS token for aws type. - STS token for aws type.
- Deprecated, please use inputs
version_added: "2.6" version_added: "2.6"
type: str type: str
secret: secret:
description: description:
- Secret token for azure_rm type. - Secret token for azure_rm type.
- Deprecated, please use inputs
type: str type: str
subscription: subscription:
description: description:
- Subscription ID for azure_rm type. - Subscription ID for azure_rm type.
- Deprecated, please use inputs
type: str type: str
tenant: tenant:
description: description:
- Tenant ID for azure_rm type. - Tenant ID for azure_rm type.
- Deprecated, please use inputs
type: str type: str
domain: domain:
description: description:
- Domain for openstack type. - Domain for openstack type.
- Deprecated, please use inputs
type: str type: str
become_method: become_method:
description: description:
- Become method to use for privilege escalation. - Become method to use for privilege escalation.
- Some examples are "None", "sudo", "su", "pbrun" - Some examples are "None", "sudo", "su", "pbrun"
- Due to become plugins, these can be arbitrary - Due to become plugins, these can be arbitrary
- Deprecated, please use inputs
type: str type: str
become_username: become_username:
description: description:
- Become username. - Become username.
- Use "ASK" and launch in Tower to be prompted. - Use "ASK" and launch in Tower to be prompted.
- Deprecated, please use inputs
type: str type: str
become_password: become_password:
description: description:
- Become password. - Become password.
- Use "ASK" and launch in Tower to be prompted. - Use "ASK" and launch in Tower to be prompted.
- Deprecated, please use inputs
type: str type: str
vault_password: vault_password:
description: description:
- Vault password. - Vault password.
- Use "ASK" and launch in Tower to be prompted. - Use "ASK" and launch in Tower to be prompted.
- Deprecated, please use inputs
type: str type: str
vault_id: vault_id:
description: description:
- Vault identifier. - Vault identifier.
- This parameter is only valid if C(kind) is specified as C(vault). - This parameter is only valid if C(kind) is specified as C(vault).
- Deprecated, please use inputs
type: str type: str
version_added: "2.8" version_added: "2.8"
state: state:
@@ -160,21 +188,28 @@ options:
choices: ["present", "absent"] choices: ["present", "absent"]
default: "present" default: "present"
type: str type: str
tower_oauthtoken:
requirements: description:
- ansible-tower-cli >= 3.0.2 - The Tower OAuth token to use.
required: False
type: str
version_added: "3.7"
extends_documentation_fragment: awx.awx.auth extends_documentation_fragment: awx.awx.auth
notes:
- Values `inputs` and the other deprecated fields (such as `tenant`) are replacements of existing values.
See the last 4 examples for details.
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Add tower credential - name: Add tower machine credential
tower_credential: tower_credential:
name: Team Name name: Team Name
description: Team Description description: Team Description
organization: test-org organization: test-org
kind: ssh credential_type: Machine
state: present state: present
tower_config_file: "~/tower_cli.cfg" tower_config_file: "~/tower_cli.cfg"
@@ -183,25 +218,25 @@ EXAMPLES = '''
name: SCM Credential name: SCM Credential
organization: Default organization: Default
state: present state: present
kind: scm credential_type: Source Control
username: joe inputs:
password: secret username: joe
ssh_key_data: "{{ lookup('file', '/tmp/id_rsa') }}" password: secret
ssh_key_unlock: "passphrase" ssh_key_data: "{{ lookup('file', '/tmp/id_rsa') }}"
ssh_key_unlock: "passphrase"
- name: Fetch private key - name: Fetch private key
slurp: slurp:
src: '$HOME/.ssh/aws-private.pem' src: '$HOME/.ssh/aws-private.pem'
register: aws_ssh_key register: aws_ssh_key
- name: Add Credential Into Tower - name: Add Credential Into Tower
tower_credential: tower_credential:
name: Workshop Credential name: Workshop Credential
ssh_key_data: "{{ aws_ssh_key['content'] | b64decode }}" credential_type: Machine
kind: ssh
organization: Default organization: Default
tower_username: admin inputs:
tower_password: ansible ssh_key_data: "{{ aws_ssh_key['content'] | b64decode }}"
tower_host: https://localhost
run_once: true run_once: true
delegate_to: localhost delegate_to: localhost
@@ -213,25 +248,47 @@ EXAMPLES = '''
tower_username: admin tower_username: admin
tower_password: ansible tower_password: ansible
tower_host: https://localhost tower_host: https://localhost
- name: Create a Vaiult credential (example for notes)
tower_credential:
name: Example password
credential_type: Vault
organization: Default
inputs:
vault_password: 'hello'
vault_id: 'My ID'
- name: Bad password update (will replace vault_id)
tower_credential:
name: Example password
credential_type: Vault
organization: Default
inputs:
vault_password: 'new_password'
- name: Another bad password update (will replace vault_id)
tower_credential:
name: Example password
credential_type: Vault
organization: Default
vault_password: 'new_password'
- name: A safe way to update a password and keep vault_id
tower_credential:
name: Example password
credential_type: Vault
organization: Default
inputs:
vault_password: 'new_password'
vault_id: 'My ID'
''' '''
import os from ..module_utils.tower_api import TowerModule
from ansible.module_utils._text import to_text
from ..module_utils.ansible_tower import TowerModule, tower_auth_config, tower_check_mode
try:
import tower_cli
import tower_cli.exceptions as exc
from tower_cli.conf import settings
except ImportError:
pass
KIND_CHOICES = { KIND_CHOICES = {
'ssh': 'Machine', 'ssh': 'Machine',
'vault': 'Ansible Vault', 'vault': 'Vault',
'net': 'Network', 'net': 'Network',
'scm': 'Source Control', 'scm': 'Source Control',
'aws': 'Amazon Web Services', 'aws': 'Amazon Web Services',
@@ -257,162 +314,118 @@ OLD_INPUT_NAMES = (
) )
def credential_type_for_kind(params):
credential_type_res = tower_cli.get_resource('credential_type')
kind = params.get('kind')
arguments = {'managed_by_tower': True}
if kind == 'ssh':
if params.get('vault_password'):
arguments['kind'] = 'vault'
else:
arguments['kind'] = 'ssh'
elif kind in ('net', 'scm', 'insights', 'vault'):
arguments['kind'] = kind
elif kind in KIND_CHOICES:
arguments.update(dict(
kind='cloud',
name=KIND_CHOICES[kind]
))
return credential_type_res.get(**arguments)
def main(): def main():
# Any additional arguments that are not fields of the item can be added here
argument_spec = dict( argument_spec = dict(
name=dict(required=True), name=dict(required=True),
user=dict(), new_name=dict(),
team=dict(), description=dict(),
kind=dict(choices=list(KIND_CHOICES.keys())), organization=dict(),
credential_type=dict(), credential_type=dict(),
inputs=dict(type='dict'), inputs=dict(type='dict'),
user=dict(),
team=dict(),
# These are for backwards compatability
kind=dict(choices=list(KIND_CHOICES.keys())),
host=dict(), host=dict(),
username=dict(), username=dict(),
password=dict(no_log=True), password=dict(no_log=True),
ssh_key_data=dict(no_log=True, type='str'), project=dict(),
ssh_key_data=dict(no_log=True),
ssh_key_unlock=dict(no_log=True), ssh_key_unlock=dict(no_log=True),
authorize=dict(type='bool', default=False), authorize=dict(type='bool'),
authorize_password=dict(no_log=True), authorize_password=dict(no_log=True),
client=dict(), client=dict(),
security_token=dict(), security_token=dict(),
secret=dict(), secret=dict(no_log=True),
tenant=dict(),
subscription=dict(), subscription=dict(),
tenant=dict(),
domain=dict(), domain=dict(),
become_method=dict(), become_method=dict(),
become_username=dict(), become_username=dict(),
become_password=dict(no_log=True), become_password=dict(no_log=True),
vault_password=dict(no_log=True), vault_password=dict(no_log=True),
description=dict(),
organization=dict(required=True),
project=dict(),
state=dict(choices=['present', 'absent'], default='present'),
vault_id=dict(), vault_id=dict(),
# End backwards compatability
state=dict(choices=['present', 'absent'], default='present'),
) )
mutually_exclusive = [ # Create a module for ourselves
('kind', 'credential_type') module = TowerModule(argument_spec=argument_spec, supports_check_mode=True, required_one_of=[['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)
# Extract our parameters
name = module.params.get('name') name = module.params.get('name')
new_name = module.params.get('new_name')
description = module.params.get('description')
organization = module.params.get('organization') organization = module.params.get('organization')
credential_type = module.params.get('credential_type')
inputs = module.params.get('inputs')
user = module.params.get('user')
team = module.params.get('team')
# The legacy arguments are put into a hash down below
kind = module.params.get('kind')
# End backwards compatability
state = module.params.get('state') state = module.params.get('state')
json_output = {'credential': name, 'state': state} # Attempt to look up the related items the user specified (these will fail the module if not found)
if organization:
org_id = module.resolve_name_to_id('organizations', organization)
if user:
user_id = module.resolve_name_to_id('users', user)
if team:
team_id = module.resolve_name_to_id('teams', team)
tower_auth = tower_auth_config(module) if kind:
with settings.runtime_values(**tower_auth): module.deprecate(msg='The kind parameter has been deprecated, please use credential_type instead', version="3.6")
tower_check_mode(module)
credential = tower_cli.get_resource('credential')
try:
params = {}
params['create_on_missing'] = True
params['name'] = name
if organization: cred_type_id = module.resolve_name_to_id('credential_types', credential_type if credential_type else KIND_CHOICES[kind])
org_res = tower_cli.get_resource('organization')
org = org_res.get(name=organization)
params['organization'] = org['id']
try: # Attempt to look up the object based on the provided name, credential type and optional organization
tower_cli.get_resource('credential_type') lookup_data = {
except (ImportError, AttributeError): 'name': name,
# /api/v1/ backwards compat 'credential_type': cred_type_id,
# older versions of tower-cli don't *have* a credential_type }
# resource if organization:
params['kind'] = module.params.get('kind') lookup_data['organization'] = org_id
else: credential = module.get_one('credentials', **{'data': lookup_data})
if module.params.get('credential_type'):
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'): # Create credential input from legacy inputs
params['inputs'] = module.params.get('inputs') credential_inputs = {}
for legacy_input in OLD_INPUT_NAMES:
if module.params.get(legacy_input) is not None:
module.deprecate(msg='{0} parameter has been deprecated, please use inputs instead'.format(legacy_input), version="3.6")
credential_inputs[legacy_input] = module.params.get(legacy_input)
if inputs:
credential_inputs.update(inputs)
elif module.params.get('kind'): # Create the data that gets sent for create and update
credential_type = credential_type_for_kind(module.params) credential_fields = {
params['credential_type'] = credential_type['id'] 'name': new_name if new_name else name,
else: 'credential_type': cred_type_id,
module.fail_json(msg='must either specify credential_type or kind', changed=False) 'inputs': credential_inputs,
}
if description:
credential_fields['description'] = description
if organization:
credential_fields['organization'] = org_id
if module.params.get('description'): # If we don't already have a credential (and we are creating one) we can add user/team
params['description'] = module.params.get('description') # The API does not appear to do anything with these after creation anyway
# NOTE: We can't just add these on a modification because they are never returned from a GET so it would always cause a changed=True
if not credential:
if user:
credential_fields['user'] = user_id
if team:
credential_fields['team'] = team_id
if module.params.get('user'): if state == 'absent':
user_res = tower_cli.get_resource('user') # If the state was absent we can let the module delete it if needed, the module will handle exiting from this
user = user_res.get(username=module.params.get('user')) module.delete_if_needed(credential)
params['user'] = user['id'] elif state == 'present':
# If the state was present we can let the module build or update the existing group, this will return on its own
if module.params.get('team'): module.create_or_update_if_needed(
team_res = tower_cli.get_resource('team') credential, credential_fields, endpoint='credentials', item_type='credential'
team = team_res.get(name=module.params.get('team')) )
params['team'] = team['id']
if module.params.get('ssh_key_data'):
data = module.params.get('ssh_key_data')
if os.path.exists(data):
module.deprecate(
msg='ssh_key_data should be a string, not a path to a file.',
version="2.12"
)
if os.path.isdir(data):
module.fail_json(msg='attempted to read contents of directory: %s' % data)
with open(data, 'rb') as f:
module.params['ssh_key_data'] = to_text(f.read())
else:
module.params['ssh_key_data'] = data
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'")
for key in OLD_INPUT_NAMES:
if 'kind' in params:
params[key] = module.params.get(key)
elif module.params.get(key):
params.setdefault('inputs', {})[key] = module.params.get(key)
if state == 'present':
result = credential.modify(**params)
json_output['id'] = result['id']
elif state == 'absent':
result = credential.delete(**params)
except (exc.NotFound) as excinfo:
module.fail_json(msg='Failed to update credential, organization not found: {0}'.format(excinfo), changed=False)
except (exc.ConnectionError, exc.BadRequest, exc.AuthError) as excinfo:
module.fail_json(msg='Failed to update credential: {0}'.format(excinfo), changed=False)
json_output['changed'] = result['changed']
module.exit_json(**json_output)
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -6,143 +6,8 @@ import pytest
from awx.main.models import Credential, CredentialType, Organization from awx.main.models import Credential, CredentialType, Organization
@pytest.mark.django_db @pytest.fixture
def test_create_machine_credential(run_module, admin_user): def cred_type():
Organization.objects.create(name='test-org')
# create the ssh credential type
ct = CredentialType.defaults['ssh']()
ct.save()
# Example from docs
result = run_module('tower_credential', dict(
name='Test Machine Credential',
organization='test-org',
kind='ssh',
state='present'
), admin_user)
assert result.get('changed'), result
cred = Credential.objects.get(name='Test Machine Credential')
assert cred.credential_type == ct
result.pop('invocation')
assert result == {
"credential": "Test Machine Credential",
"state": "present",
"id": cred.pk,
"changed": True
}
@pytest.mark.django_db
def test_create_vault_credential(run_module, admin_user):
# https://github.com/ansible/ansible/issues/61324
Organization.objects.create(name='test-org')
ct = CredentialType.defaults['vault']()
ct.save()
result = run_module('tower_credential', dict(
name='Test Vault Credential',
organization='test-org',
kind='vault',
vault_id='bar',
vault_password='foobar',
state='present'
), admin_user)
assert result.get('changed'), result
cred = Credential.objects.get(name='Test Vault Credential')
assert cred.credential_type == ct
assert 'vault_id' in cred.inputs
assert 'vault_password' in cred.inputs
result.pop('invocation')
assert result == {
"credential": "Test Vault Credential",
"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)
assert result.get('changed'), result
ct = CredentialType.objects.get(name='Nexus')
result.pop('invocation')
assert result == {
"name": "Nexus",
"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 # Make a credential type which will be used by the credential
ct = CredentialType.objects.create( ct = CredentialType.objects.create(
name='Ansible Galaxy Token', name='Ansible Galaxy Token',
@@ -163,23 +28,149 @@ def test_make_use_of_custom_credential_type(run_module, admin_user):
} }
} }
) )
return ct
@pytest.mark.django_db
def test_create_machine_credential(run_module, admin_user, organization):
Organization.objects.create(name='test-org')
# create the ssh credential type
ct = CredentialType.defaults['ssh']()
ct.save()
# Example from docs
result = run_module('tower_credential', dict(
name='Test Machine Credential',
organization=organization.name,
kind='ssh',
state='present'
), admin_user)
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
cred = Credential.objects.get(name='Test Machine Credential')
assert cred.credential_type == ct
assert result['name'] == "Test Machine Credential"
assert result['id'] == cred.pk
@pytest.mark.django_db
def test_create_vault_credential(run_module, admin_user, organization):
# https://github.com/ansible/ansible/issues/61324
Organization.objects.create(name='test-org')
ct = CredentialType.defaults['vault']()
ct.save()
result = run_module('tower_credential', dict(
name='Test Vault Credential',
organization=organization.name,
kind='vault',
vault_id='bar',
vault_password='foobar',
state='present'
), admin_user)
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
cred = Credential.objects.get(name='Test Vault Credential')
assert cred.credential_type == ct
assert 'vault_id' in cred.inputs
assert 'vault_password' in cred.inputs
assert result['name'] == "Test Vault Credential"
assert result['id'] == cred.pk
@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)
assert not result.get('failed', False), result.get('msg', result)
assert result.get('changed'), result
ct = CredentialType.objects.get(name='Nexus')
assert result['name'] == 'Nexus'
assert result['id'] == ct.pk
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_ct_precedence_over_kind(run_module, admin_user, organization, cred_type, silence_deprecation):
result = run_module('tower_credential', dict(
name='A credential',
organization=organization.name,
kind='ssh',
credential_type=cred_type.name,
state='present'
), admin_user)
assert not result.get('failed', False), result.get('msg', result)
cred = Credential.objects.get(name='A credential')
assert cred.credential_type == cred_type
@pytest.mark.django_db
def test_input_overrides_old_fields(run_module, admin_user, organization):
# create the vault credential type
ct = CredentialType.defaults['vault']()
ct.save()
result = run_module('tower_credential', dict(
name='A Vault credential',
organization=organization.name,
kind='vault',
vault_id='1234',
inputs={'vault_id': 'asdf'},
state='present',
), admin_user)
assert not result.get('failed', False), result.get('msg', result)
cred = Credential.objects.get(name='A Vault credential')
assert cred.inputs['vault_id'] == 'asdf'
@pytest.mark.django_db
def test_missing_credential_type(run_module, admin_user, organization):
Organization.objects.create(name='test-org')
result = run_module('tower_credential', dict(
name='A credential',
organization=organization.name,
credential_type='foobar',
state='present'
), admin_user)
assert result.get('failed', False), result
assert 'foobar was not found on the Tower server' in result['msg']
@pytest.mark.django_db
def test_make_use_of_custom_credential_type(run_module, admin_user, cred_type):
Organization.objects.create(name='test-org')
result = run_module('tower_credential', dict( result = run_module('tower_credential', dict(
name='Galaxy Token for Steve', name='Galaxy Token for Steve',
organization='test-org', organization='test-org',
credential_type='Ansible Galaxy Token', credential_type=cred_type.name,
inputs={'token': '7rEZK38DJl58A7RxA6EC7lLvUHbBQ1'}, inputs={'token': '7rEZK38DJl58A7RxA6EC7lLvUHbBQ1'},
state='present' state='present'
), admin_user) ), admin_user)
cred = Credential.objects.get(name='Galaxy Token for Steve') cred = Credential.objects.get(name='Galaxy Token for Steve')
assert cred.credential_type_id == ct.id assert cred.credential_type_id == cred_type.id
assert list(cred.inputs.keys()) == ['token'] assert list(cred.inputs.keys()) == ['token']
assert cred.inputs['token'].startswith('$encrypted$') assert cred.inputs['token'].startswith('$encrypted$')
assert len(cred.inputs['token']) >= len('$encrypted$') + len('7rEZK38DJl58A7RxA6EC7lLvUHbBQ1') assert len(cred.inputs['token']) >= len('$encrypted$') + len('7rEZK38DJl58A7RxA6EC7lLvUHbBQ1')
result.pop('invocation')
assert result == { assert result['name'] == "Galaxy Token for Steve"
"credential": "Galaxy Token for Steve", assert result['id'] == cred.pk
"state": "present",
"id": cred.pk,
"changed": True
}

View File

@@ -1,24 +1,29 @@
--- ---
- name: Generate a random string for test
set_fact:
test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}"
when: test_id is not defined
- name: Generate names - name: Generate names
set_fact: set_fact:
ssh_cred_name1: "AWX-Collection-tests-tower_credential-ssh-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" ssh_cred_name1: "AWX-Collection-tests-tower_credential-ssh-cred1-{{ test_id }}"
ssh_cred_name2: "AWX-Collection-tests-tower_credential-ssh-cred2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" ssh_cred_name2: "AWX-Collection-tests-tower_credential-ssh-cred2-{{ test_id }}"
ssh_cred_name3: "AWX-Collection-tests-tower_credential-ssh-cred-lookup-source-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" ssh_cred_name3: "AWX-Collection-tests-tower_credential-ssh-cred-lookup-source-{{ test_id }}"
ssh_cred_name4: "AWX-Collection-tests-tower_credential-ssh-cred-file-source-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" ssh_cred_name4: "AWX-Collection-tests-tower_credential-ssh-cred-file-source-{{ test_id }}"
vault_cred_name1: "AWX-Collection-tests-tower_credential-vault-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" vault_cred_name1: "AWX-Collection-tests-tower_credential-vault-cred1-{{ test_id }}"
vault_cred_name2: "AWX-Collection-tests-tower_credential-vault-ssh-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" vault_cred_name2: "AWX-Collection-tests-tower_credential-vault-ssh-cred1-{{ test_id }}"
net_cred_name1: "AWX-Collection-tests-tower_credential-net-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" net_cred_name1: "AWX-Collection-tests-tower_credential-net-cred1-{{ test_id }}"
scm_cred_name1: "AWX-Collection-tests-tower_credential-scm-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" scm_cred_name1: "AWX-Collection-tests-tower_credential-scm-cred1-{{ test_id }}"
aws_cred_name1: "AWX-Collection-tests-tower_credential-aws-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" aws_cred_name1: "AWX-Collection-tests-tower_credential-aws-cred1-{{ test_id }}"
vmware_cred_name1: "AWX-Collection-tests-tower_credential-vmware-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" vmware_cred_name1: "AWX-Collection-tests-tower_credential-vmware-cred1-{{ test_id }}"
sat6_cred_name1: "AWX-Collection-tests-tower_credential-sat6-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" sat6_cred_name1: "AWX-Collection-tests-tower_credential-sat6-cred1-{{ test_id }}"
cf_cred_name1: "AWX-Collection-tests-tower_credential-cf-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" cf_cred_name1: "AWX-Collection-tests-tower_credential-cf-cred1-{{ test_id }}"
gce_cred_name1: "AWX-Collection-tests-tower_credential-gce-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" gce_cred_name1: "AWX-Collection-tests-tower_credential-gce-cred1-{{ test_id }}"
azurerm_cred_name1: "AWX-Collection-tests-tower_credential-azurerm-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" azurerm_cred_name1: "AWX-Collection-tests-tower_credential-azurerm-cred1-{{ test_id }}"
openstack_cred_name1: "AWX-Collection-tests-tower_credential-openstack-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" openstack_cred_name1: "AWX-Collection-tests-tower_credential-openstack-cred1-{{ test_id }}"
rhv_cred_name1: "AWX-Collection-tests-tower_credential-rhv-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" rhv_cred_name1: "AWX-Collection-tests-tower_credential-rhv-cred1-{{ test_id }}"
insights_cred_name1: "AWX-Collection-tests-tower_credential-insights-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" insights_cred_name1: "AWX-Collection-tests-tower_credential-insights-cred1-{{ test_id }}"
tower_cred_name1: "AWX-Collection-tests-tower_credential-tower-cred1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" tower_cred_name1: "AWX-Collection-tests-tower_credential-tower-cred1-{{ test_id }}"
- name: create a tempdir for an SSH key - name: create a tempdir for an SSH key
local_action: shell mktemp -d local_action: shell mktemp -d
@@ -31,7 +36,42 @@
set_fact: set_fact:
ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}" ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}"
- name: Create a User-specific credential - name: Test deprecation warnings
tower_credential:
name: "{{ ssh_cred_name1 }}"
organization: Default
user: admin
kind: ssh
authorize: false
authorize_password: 'test'
client: 'test'
security_token: 'test'
secret: 'test'
tenant: 'test'
subscription: 'test'
domain: 'test'
become_method: 'test'
become_username: 'test'
become_password: 'test'
vault_password: 'test'
project: 'test'
host: 'test'
username: 'test'
password: 'test'
ssh_key_data: 'test'
vault_id: 'test'
ssh_key_unlock: 'test'
state: absent
ignore_errors: true
register: result
- assert:
that:
- "'deprecations' in result"
# The 20 comes from the length of OLD_INPUT_NAMES + 1 for kind
- result['deprecations'] | length() == 20
- name: Create a User-specific credential (old school)
tower_credential: tower_credential:
name: "{{ ssh_cred_name1 }}" name: "{{ ssh_cred_name1 }}"
organization: Default organization: Default
@@ -44,6 +84,19 @@
that: that:
- "result is changed" - "result is changed"
- name: Re-create the User-specific credential (new school)
tower_credential:
name: "{{ ssh_cred_name1 }}"
organization: Default
user: admin
credential_type: 'Machine'
state: present
register: result
- assert:
that:
- "result is not changed"
- name: Delete a User-specific credential - name: Delete a User-specific credential
tower_credential: tower_credential:
name: "{{ ssh_cred_name1 }}" name: "{{ ssh_cred_name1 }}"
@@ -57,7 +110,31 @@
that: that:
- "result is changed" - "result is changed"
- name: Create a valid SSH credential - name: Create the User-specific credential tied to a user, no org
tower_credential:
name: "{{ ssh_cred_name1 }}"
user: admin
credential_type: 'Machine'
state: present
register: result
- assert:
that:
- "result is changed"
- name: Delete a User-specific credential
tower_credential:
name: "{{ ssh_cred_name1 }}"
user: admin
state: absent
kind: ssh
register: result
- assert:
that:
- "result is changed"
- name: Create a valid SSH credential (old school)
tower_credential: tower_credential:
name: "{{ ssh_cred_name2 }}" name: "{{ ssh_cred_name2 }}"
organization: Default organization: Default
@@ -77,7 +154,48 @@
that: that:
- "result is changed" - "result is changed"
- name: Create a valid SSH credential from lookup source - name: Create a valid SSH credential (new school)
tower_credential:
name: "{{ ssh_cred_name2 }}"
organization: Default
state: present
credential_type: Machine
description: An example SSH credential
inputs:
username: joe
password: secret
become_method: sudo
become_username: superuser
become_password: supersecret
ssh_key_data: "{{ ssh_key_data }}"
ssh_key_unlock: "passphrase"
register: result
# This will be changed because we are setting ssh_key_data and ssh_key_unlock.
# These will come out as $encrypted$ which will always compare false to the values.
- assert:
that:
- result is changed
- name: Create a valid SSH credential (new school)
tower_credential:
name: "{{ ssh_cred_name2 }}"
organization: Default
state: present
credential_type: Machine
description: An example SSH credential
inputs:
username: joe
become_method: sudo
become_username: superuser
register: result
# This shows as "changed" because these listed inputs replace the existing inputs from the previous task
- assert:
that:
- result is changed
- name: Create a valid SSH credential from lookup source (old school)
tower_credential: tower_credential:
name: "{{ ssh_cred_name3 }}" name: "{{ ssh_cred_name3 }}"
organization: Default organization: Default
@@ -97,7 +215,29 @@
that: that:
- "result is changed" - "result is changed"
- name: Create a valid SSH credential from file source - name: Create a valid SSH credential from lookup source (new school)
tower_credential:
name: "{{ ssh_cred_name3 }}"
organization: Default
state: present
credential_type: Machine
description: An example SSH credential from lookup source
inputs:
username: joe
password: secret
become_method: sudo
become_username: superuser
become_password: supersecret
ssh_key_data: "{{ lookup('file', tempdir.stdout + '/id_rsa') }}"
ssh_key_unlock: "passphrase"
register: result
# This will be changed because we are passing in ssh_key_data and password
- assert:
that:
- result is changed
- name: Fail to create an SSH credential from a file source (old school format)
tower_credential: tower_credential:
name: "{{ ssh_cred_name4 }}" name: "{{ ssh_cred_name4 }}"
organization: Default organization: Default
@@ -112,12 +252,13 @@
ssh_key_data: "{{ tempdir.stdout }}/id_rsa" ssh_key_data: "{{ tempdir.stdout }}/id_rsa"
ssh_key_unlock: "passphrase" ssh_key_unlock: "passphrase"
register: result register: result
ignore_errors: true
- assert: - assert:
that: that:
- "result is changed" - result is failed
- "result is not failed" - "'Unable to create credential {{ ssh_cred_name4 }}' in result.msg"
- "'ssh_key_data should be a string, not a path to a file.' in result.deprecations[0].msg" - "'Invalid certificate or key' in result.msg"
- name: Create an invalid SSH credential (passphrase required) - name: Create an invalid SSH credential (passphrase required)
tower_credential: tower_credential:
@@ -148,7 +289,7 @@
- assert: - assert:
that: that:
- "result is failed" - "result is failed"
- "'The requested object could not be found' in result.msg" - "'The organizations Missing Organization was not found on the Tower server' in result.msg"
- name: Delete an SSH credential - name: Delete an SSH credential
tower_credential: tower_credential:
@@ -182,9 +323,10 @@
kind: ssh kind: ssh
register: result register: result
# This one was never really created so it shouldn't be deleted
- assert: - assert:
that: that:
- "result is changed" - "result is not changed"
- name: Create a valid Vault credential - name: Create a valid Vault credential
tower_credential: tower_credential:
@@ -201,7 +343,7 @@
- "result is changed" - "result is changed"
# We should decide when to delete this test # We should decide when to delete this test
- name: Create a valid Vault credential w/ kind=ssh (deprecated) - name: Create a valid Vault credential w/ kind=ssh (deprecated, will now fail)
tower_credential: tower_credential:
name: "{{ vault_cred_name2 }}" name: "{{ vault_cred_name2 }}"
organization: Default organization: Default
@@ -210,10 +352,14 @@
description: An example Vault credential description: An example Vault credential
vault_password: secret-vault vault_password: secret-vault
register: result register: result
ignore_errors: true
- assert: - assert:
that: that:
- "result is changed" - result is failed
- "'Unable to create credential {{ vault_cred_name2 }}' in result.msg"
- "'Additional properties are not allowed' in result.msg"
- "'\\'vault_password\\' was unexpected' in result.msg"
- name: Delete a Vault credential - name: Delete a Vault credential
tower_credential: tower_credential:
@@ -235,9 +381,10 @@
kind: vault kind: vault
register: result register: result
# The creation of vault_cred_name2 never worked so we shouldn't actually need to delete it
- assert: - assert:
that: that:
- "result is changed" - "result is not changed"
- name: Create a valid Network credential - name: Create a valid Network credential
tower_credential: tower_credential:
@@ -594,4 +741,5 @@
- assert: - assert:
that: that:
- "result.msg =='Failed to update credential, organization not found: The requested object could not be found.'" - result is failed
- "result.msg =='The organizations test-non-existing-org was not found on the Tower server'"