diff --git a/awx/main/constants.py b/awx/main/constants.py index 9c0b01a3d4..aabcc0b2e3 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -5,7 +5,7 @@ import re from django.utils.translation import ugettext_lazy as _ -CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'ovirt4', 'satellite6', 'cloudforms') +CLOUD_PROVIDERS = ('azure_rm', 'ec2', 'gce', 'vmware', 'openstack', 'ovirt4', 'satellite6', 'cloudforms', 'tower') SCHEDULEABLE_PROVIDERS = CLOUD_PROVIDERS + ('custom', 'scm',) PRIVILEGE_ESCALATION_METHODS = [ ('sudo', _('Sudo')), ('su', _('Su')), ('pbrun', _('Pbrun')), ('pfexec', _('Pfexec')), ('dzdo', _('DZDO')), ('pmrun', _('Pmrun')), ('runas', _('Runas'))] ANSI_SGR_PATTERN = re.compile(r'\x1b\[[0-9;]*m') diff --git a/awx/main/migrations/0010_v322_add_support_for_ovirt4_inventory.py b/awx/main/migrations/0010_v322_add_ovirt4_tower_inventory.py similarity index 87% rename from awx/main/migrations/0010_v322_add_support_for_ovirt4_inventory.py rename to awx/main/migrations/0010_v322_add_ovirt4_tower_inventory.py index e6db61e4b8..aa00d66163 100644 --- a/awx/main/migrations/0010_v322_add_support_for_ovirt4_inventory.py +++ b/awx/main/migrations/0010_v322_add_ovirt4_tower_inventory.py @@ -14,15 +14,15 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RunPython(credentialtypes.create_ovirt4_credtype), + migrations.RunPython(credentialtypes.create_ovirt4_tower_credtype), migrations.AlterField( model_name='inventorysource', name='source', - field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a Project'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'ovirt4', 'oVirt4'), (b'custom', 'Custom Script')]), + field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a Project'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'ovirt4', 'oVirt4'), (b'tower', 'Ansible Tower'), (b'custom', 'Custom Script')]), ), migrations.AlterField( model_name='inventoryupdate', name='source', - field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a Project'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'ovirt4', 'oVirt4'), (b'custom', 'Custom Script')]), + field=models.CharField(default=b'', max_length=32, blank=True, choices=[(b'', 'Manual'), (b'file', 'File, Directory or Script'), (b'scm', 'Sourced from a Project'), (b'ec2', 'Amazon EC2'), (b'gce', 'Google Compute Engine'), (b'azure_rm', 'Microsoft Azure Resource Manager'), (b'vmware', 'VMware vCenter'), (b'satellite6', 'Red Hat Satellite 6'), (b'cloudforms', 'Red Hat CloudForms'), (b'openstack', 'OpenStack'), (b'ovirt4', 'oVirt4'), (b'tower', 'Ansible Tower'), (b'custom', 'Custom Script')]), ), ] diff --git a/awx/main/migrations/0011_v322_encrypt_survey_passwords.py b/awx/main/migrations/0011_v322_encrypt_survey_passwords.py index 344aa96bc1..7140949c7e 100644 --- a/awx/main/migrations/0011_v322_encrypt_survey_passwords.py +++ b/awx/main/migrations/0011_v322_encrypt_survey_passwords.py @@ -9,7 +9,7 @@ from awx.main.migrations import _reencrypt as reencrypt class Migration(ActivityStreamDisabledMigration): dependencies = [ - ('main', '0010_v322_add_support_for_ovirt4_inventory'), + ('main', '0010_v322_add_ovirt4_tower_inventory'), ] operations = [ diff --git a/awx/main/migrations/_credentialtypes.py b/awx/main/migrations/_credentialtypes.py index 104caa334a..3046debd80 100644 --- a/awx/main/migrations/_credentialtypes.py +++ b/awx/main/migrations/_credentialtypes.py @@ -174,5 +174,5 @@ def migrate_job_credentials(apps, schema_editor): utils.get_current_apps = orig_current_apps -def create_ovirt4_credtype(apps, schema_editor): +def create_ovirt4_tower_credtype(apps, schema_editor): CredentialType.setup_tower_managed_defaults() diff --git a/awx/main/models/base.py b/awx/main/models/base.py index 93bb484a46..9c7f58d096 100644 --- a/awx/main/models/base.py +++ b/awx/main/models/base.py @@ -52,7 +52,7 @@ PROJECT_UPDATE_JOB_TYPE_CHOICES = [ (PERM_INVENTORY_CHECK, _('Check')), ] -CLOUD_INVENTORY_SOURCES = ['ec2', 'vmware', 'gce', 'azure_rm', 'openstack', 'ovirt4', 'custom', 'satellite6', 'cloudforms', 'scm',] +CLOUD_INVENTORY_SOURCES = ['ec2', 'vmware', 'gce', 'azure_rm', 'openstack', 'ovirt4', 'custom', 'satellite6', 'cloudforms', 'scm', 'tower',] VERBOSITY_CHOICES = [ (0, '0 (Normal)'), diff --git a/awx/main/models/credential.py b/awx/main/models/credential.py index 77ce9ca3b0..e4ee83fdb7 100644 --- a/awx/main/models/credential.py +++ b/awx/main/models/credential.py @@ -64,6 +64,7 @@ class V1Credential(object): ('openstack', 'OpenStack'), ('ovirt4', 'oVirt4'), ('insights', 'Insights'), + ('tower', 'Ansible Tower'), ] FIELDS = { 'kind': models.CharField( @@ -1061,3 +1062,37 @@ def ovirt4(cls): } }, ) + + +@CredentialType.default +def tower(cls): + return cls( + kind='cloud', + name='Ansible Tower', + managed_by_tower=True, + inputs={ + 'fields': [{ + 'id': 'host', + 'label': 'Ansible Tower Hostname', + 'type': 'string', + 'help_text': ('The Ansible Tower base URL to authenticate with.') + }, { + 'id': 'username', + 'label': 'Username', + 'type': 'string' + }, { + 'id': 'password', + 'label': 'Password', + 'type': 'string', + 'secret': True, + }], + 'required': ['host', 'username', 'password'], + }, + injectors={ + 'env': { + 'TOWER_HOSTNAME': '{{host}}', + 'TOWER_USERNAME': '{{username}}', + 'TOWER_PASSWORD': '{{password}}' + } + }, + ) diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index f5cd0d8e58..e6d2164750 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -873,6 +873,7 @@ class InventorySourceOptions(BaseModel): ('cloudforms', _('Red Hat CloudForms')), ('openstack', _('OpenStack')), ('ovirt4', _('oVirt4')), + ('tower', _('Ansible Tower')), ('custom', _('Custom Script')), ] @@ -1126,6 +1127,11 @@ class InventorySourceOptions(BaseModel): """No region supprt""" return [('all', 'All')] + @classmethod + def get_tower_region_choices(self): + """No region supprt""" + return [('all', 'All')] + def clean_credential(self): if not self.source: return None diff --git a/awx/plugins/inventory/tower.py b/awx/plugins/inventory/tower.py new file mode 100755 index 0000000000..86121ca24e --- /dev/null +++ b/awx/plugins/inventory/tower.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# +# Copyright (c) 2016 Red Hat, Inc. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# +""" +Ansible Tower/AWX dynamic inventory script +========================================== + +Generates dynamic inventory for Tower + +Author: Matthew Jones (@matburt) +""" + +import argparse +import re +import os +import sys +import json +import requests +from requests.auth import HTTPBasicAuth +from urlparse import urljoin + + +def parse_configuration(): + """ + Create command line parser for oVirt dynamic inventory script. + """ + parser = argparse.ArgumentParser( + description='Ansible dynamic inventory script for Ansible Tower.', + ) + parser.add_argument( + '--list', + action='store_true', + default=True, + help='Return all hosts known to Tower given a particular inventory', + ) + parser.parse_args() + host_name = os.environ.get("TOWER_HOSTNAME", None) + username = os.environ.get("TOWER_USERNAME", None) + password = os.environ.get("TOWER_PASSWORD", None) + ignore_ssl = os.environ.get("TOWER_IGNORE_SSL", "0").lower() in ("1", "yes", "true") + inventory = os.environ.get("TOWER_INVENTORY", None) + + errors = [] + if not host_name: + errors.append("Missing TOWER_HOSTNAME in environment") + if not username: + errors.append("Missing TOWER_USERNAME in environment") + if not password: + errors.append("Missing TOWER_PASSWORD in environment") + if not inventory: + errors.append("Missing TOWER_INVENTORY in environment") + if errors: + print("\n".join(errors)) + sys.exit(1) + + return dict(tower_host=host_name, + tower_user=username, + tower_pass=password, + tower_inventory=inventory, + ignore_ssl=ignore_ssl) + + +def read_tower_inventory(tower_host, tower_user, tower_pass, inventory, ignore_ssl=False): + if not re.match('(?:http|https)://', tower_host): + tower_host = "https://{}".format(tower_host) + inventory_url = urljoin(tower_host, "/api/v2/inventories/{}/script/?hostvars=1".format(inventory)) + try: + response = requests.get(inventory_url, + auth=HTTPBasicAuth(tower_user, tower_pass), + verify=not ignore_ssl) + if response.ok: + return response.json() + json_reason = response.json() + reason = json_reason.get('detail', 'Retrieving Tower Inventory Failed') + except requests.ConnectionError, e: + reason = "Connection to remote host failed: {}".format(e) + print(reason) + sys.exit(1) + + +def main(): + config = parse_configuration() + inventory_hosts = read_tower_inventory(config['tower_host'], + config['tower_user'], + config['tower_pass'], + config['tower_inventory'], + ignore_ssl=config['ignore_ssl']) + print( + json.dumps( + inventory_hosts + ) + ) + +if __name__ == '__main__': + main()