Merge pull request #551 from ansible/tower_inventory_source

Tower inventory source
This commit is contained in:
Matthew Jones 2017-10-27 08:41:24 -04:00 committed by GitHub
commit f019452207
16 changed files with 210 additions and 14 deletions

View File

@ -2377,6 +2377,7 @@ class InventoryScriptView(RetrieveAPIView):
obj = self.get_object()
hostname = request.query_params.get('host', '')
hostvars = bool(request.query_params.get('hostvars', ''))
towervars = bool(request.query_params.get('towervars', ''))
show_all = bool(request.query_params.get('all', ''))
if show_all:
hosts_q = dict()
@ -2441,6 +2442,10 @@ class InventoryScriptView(RetrieveAPIView):
data['_meta'].setdefault('hostvars', dict())
for host in obj.hosts.filter(**hosts_q):
data['_meta']['hostvars'][host.name] = host.variables_dict
if towervars:
tower_dict = dict(remote_tower_enabled=str(host.enabled).lower(),
remote_tower_id=host.id)
data['_meta']['hostvars'][host.name].update(tower_dict)
return Response(data)

View File

@ -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')

View File

@ -7,7 +7,7 @@ import logging
from django.db import models
from django.utils.timezone import now
from django.db.models import Sum
from django.db.models import Sum, Q
from django.conf import settings
from awx.main.utils.filters import SmartFilter
@ -21,9 +21,9 @@ class HostManager(models.Manager):
"""Custom manager class for Hosts model."""
def active_count(self):
"""Return count of active, unique hosts for licensing."""
"""Return count of active, unique hosts for licensing. Exclude ones source from another Tower"""
try:
return self.order_by('name').distinct('name').count()
return self.filter(~Q(inventory_sources__source='tower')).order_by('name').distinct('name').count()
except NotImplementedError: # For unit tests only, SQLite doesn't support distinct('name')
return len(set(self.values_list('name', flat=True)))

View File

@ -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')]),
),
]

View File

@ -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 = [

View File

@ -174,7 +174,7 @@ 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()

View File

@ -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)'),

View File

@ -64,6 +64,7 @@ class V1Credential(object):
('openstack', 'OpenStack'),
('ovirt4', 'oVirt4'),
('insights', 'Insights'),
('tower', 'Ansible Tower'),
]
FIELDS = {
'kind': models.CharField(
@ -1067,3 +1068,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_HOST': '{{host}}',
'TOWER_USERNAME': '{{username}}',
'TOWER_PASSWORD': '{{password}}',
}
},
)

View File

@ -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
@ -1197,7 +1203,7 @@ class InventorySourceOptions(BaseModel):
raise ValidationError(_('Invalid filter expression: %(filter)s') %
{'filter': ', '.join(invalid_filters)})
return instance_filters
elif self.source == 'vmware':
elif self.source in ('vmware', 'tower'):
return instance_filters
else:
return ''

View File

@ -1918,6 +1918,9 @@ class RunInventoryUpdate(BaseTask):
for env_k in inventory_update.source_vars_dict:
if str(env_k) not in env and str(env_k) not in settings.INV_ENV_VARIABLE_BLACKLIST:
env[str(env_k)] = unicode(inventory_update.source_vars_dict[env_k])
elif inventory_update.source == 'tower':
env['TOWER_INVENTORY'] = inventory_update.instance_filters
env['TOWER_LICENSE_TYPE'] = get_licenser().validate()['license_type']
elif inventory_update.source == 'file':
raise NotImplementedError('Cannot update file sources through the task system.')
# add private_data_files

View File

@ -29,6 +29,7 @@ def test_default_cred_types():
'satellite6',
'scm',
'ssh',
'tower',
'vault',
'vmware',
]

130
awx/plugins/inventory/tower.py Executable file
View File

@ -0,0 +1,130 @@
#!/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 <http://www.gnu.org/licenses/>.
#
"""
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_HOST", None)
username = os.environ.get("TOWER_USERNAME", None)
password = os.environ.get("TOWER_PASSWORD", None)
ignore_ssl = os.environ.get("TOWER_IGNORE_SSL", "1").lower() in ("1", "yes", "true")
inventory = os.environ.get("TOWER_INVENTORY", None)
license_type = os.environ.get("TOWER_LICENSE_TYPE", "enterprise")
errors = []
if not host_name:
errors.append("Missing TOWER_HOST 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,
tower_license_type=license_type,
ignore_ssl=ignore_ssl)
def read_tower_inventory(tower_host, tower_user, tower_pass, inventory, license_type, 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&towervars=1&all=1".format(inventory.replace('/', '')))
config_url = urljoin(tower_host, "/api/v2/config/")
try:
if license_type != "open":
config_response = requests.get(config_url,
auth=HTTPBasicAuth(tower_user, tower_pass),
verify=not ignore_ssl)
if config_response.ok:
source_type = config_response.json()['license_info']['license_type']
if not source_type == license_type:
print("Tower server licenses must match: source: {} local: {}".format(source_type,
license_type))
sys.exit(1)
else:
print("Failed to validate the license of the remote Tower: {}".format(config_response.data))
sys.exit(1)
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'],
config['tower_license_type'],
ignore_ssl=config['ignore_ssl'])
print(
json.dumps(
inventory_hosts
)
)
if __name__ == '__main__':
main()

View File

@ -832,6 +832,16 @@ OVIRT4_HOST_FILTER = r'^.+$'
OVIRT4_EXCLUDE_EMPTY_GROUPS = True
OVIRT4_INSTANCE_ID_VAR = 'id'
# ---------------------
# ----- Tower -----
# ---------------------
TOWER_ENABLED_VAR = 'remote_tower_enabled'
TOWER_ENABLED_VALUE = 'true'
TOWER_GROUP_FILTER = r'^.+$'
TOWER_HOST_FILTER = r'^.+$'
TOWER_EXCLUDE_EMPTY_GROUPS = True
TOWER_INSTANCE_ID_VAR = 'remote_tower_id'
# ---------------------
# ----- Foreman -----
# ---------------------

View File

@ -198,7 +198,10 @@ export default ['$state', '$stateParams', '$scope', 'SourcesFormDefinition',
$scope.group_by = $scope.group_by_choices;
$scope.groupByPopOver = i18n._("Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.");
$scope.instanceFilterPopOver = i18n._("Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail.");
}
}
if( _.get($scope, 'source') === 'tower' || _.get($scope.source, 'value') === 'tower') {
$scope.instanceFilterPopOver = i18n._("Provide the name or id of the remote Tower inventory to be imported.");
}
CreateSelect2({
element: '#inventory_source_group_by',
multiple: true,

View File

@ -275,7 +275,7 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString',
i18n._("for a complete list of supported filters.") + "</p>";
}
if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') {
if( _.get($scope, 'source') === 'vmware' || _.get($scope.source, 'value') === 'vmware') {
add_new = true;
$scope.group_by_choices = (inventorySourceData.group_by) ? inventorySourceData.group_by.split(',')
.map((i) => ({name: i, label: i, value: i})) : [];
@ -283,6 +283,9 @@ export default ['$state', '$stateParams', '$scope', 'ParseVariableString',
$scope.groupByPopOver = i18n._(`Specify which groups to create automatically. Group names will be created similar to the options selected. If blank, all groups above are created. Refer to Ansible Tower documentation for more detail.`);
$scope.instanceFilterPopOver = i18n._(`Provide a comma-separated list of filter expressions. Hosts are imported when all of the filters match. Refer to Ansible Tower documentation for more detail.`);
}
if( _.get($scope, 'source') === 'tower' || _.get($scope.source, 'value') === 'tower') {
$scope.instanceFilterPopOver = i18n._(`Provide the name or id of the remote Tower inventory to be imported.`);
}
CreateSelect2({
element: '#inventory_source_group_by',
multiple: true,

View File

@ -144,7 +144,7 @@ return {
instance_filters: {
label: i18n._("Instance Filters"),
type: 'text',
ngShow: "source && (source.value == 'ec2' || source.value == 'vmware')",
ngShow: "source && (source.value == 'ec2' || source.value == 'vmware' || source.value == 'tower')",
dataTitle: i18n._('Instance Filters'),
dataPlacement: 'right',
awPopOverWatch: 'instanceFilterPopOver',