mirror of
https://github.com/ansible/awx.git
synced 2026-01-16 04:10:44 -03:30
Merge pull request #551 from ansible/tower_inventory_source
Tower inventory source
This commit is contained in:
commit
f019452207
@ -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)
|
||||
|
||||
|
||||
@ -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')
|
||||
|
||||
@ -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)))
|
||||
|
||||
|
||||
@ -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')]),
|
||||
),
|
||||
]
|
||||
@ -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 = [
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
|
||||
@ -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)'),
|
||||
|
||||
@ -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}}',
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
@ -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 ''
|
||||
|
||||
@ -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
|
||||
|
||||
@ -29,6 +29,7 @@ def test_default_cred_types():
|
||||
'satellite6',
|
||||
'scm',
|
||||
'ssh',
|
||||
'tower',
|
||||
'vault',
|
||||
'vmware',
|
||||
]
|
||||
|
||||
130
awx/plugins/inventory/tower.py
Executable file
130
awx/plugins/inventory/tower.py
Executable 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()
|
||||
@ -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 -----
|
||||
# ---------------------
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user