awx/awx/main/utils/mem_inventory.py
AlanCoding 8e6020436c modularization of inventory_import command
This separates file parsing logic that was mixed
in with other important code inside of the
inventory import command.

The logic around MemObject data structures was
moved to utils, and the file parsing was moved
to a legacy module. As of this commit, that
module can operate within the Tower environment
but it will be removed.

Also refactor the loggers to fix old bug and
work inside of the different contexts - the
Loader classes, mem objects, and hopefully
the inventory modules eventually.
2017-05-01 14:37:42 -04:00

316 lines
11 KiB
Python

# Copyright (c) 2017 Ansible by Red Hat
# All Rights Reserved.
# Python
import re
import logging
from collections import OrderedDict
# Logger is used for any data-related messages so that the log level
# can be adjusted on command invocation
logger = logging.getLogger('awx.main.commands.inventory_import')
__all__ = ['MemHost', 'MemGroup', 'MemInventory',
'mem_data_to_dict', 'dict_to_mem_data']
ipv6_port_re = re.compile(r'^\[([A-Fa-f0-9:]{3,})\]:(\d+?)$')
# Models for in-memory objects that represent an inventory
class MemObject(object):
'''
Common code shared between in-memory groups and hosts.
'''
def __init__(self, name):
assert name, 'no name'
self.name = name
class MemGroup(MemObject):
'''
In-memory representation of an inventory group.
'''
def __init__(self, name):
super(MemGroup, self).__init__(name)
self.children = []
self.hosts = []
self.variables = {}
self.parents = []
# Used on the "all" group in place of previous global variables.
# maps host and group names to hosts to prevent redudant additions
self.all_hosts = {}
self.all_groups = {}
self.variables = {}
logger.debug('Loaded group: %s', self.name)
def __repr__(self):
return '<_in-memory-group_ `{}`>'.format(self.name)
def add_child_group(self, group):
assert group.name is not 'all', 'group name is all'
assert isinstance(group, MemGroup), 'not MemGroup instance'
logger.debug('Adding child group %s to parent %s', group.name, self.name)
if group not in self.children:
self.children.append(group)
if self not in group.parents:
group.parents.append(self)
def add_host(self, host):
assert isinstance(host, MemHost), 'not MemHost instance'
logger.debug('Adding host %s to group %s', host.name, self.name)
if host not in self.hosts:
self.hosts.append(host)
def debug_tree(self, group_names=None):
group_names = group_names or set()
if self.name in group_names:
return
logger.debug('Dumping tree for group "%s":', self.name)
logger.debug('- Vars: %r', self.variables)
for h in self.hosts:
logger.debug('- Host: %s, %r', h.name, h.variables)
for g in self.children:
logger.debug('- Child: %s', g.name)
logger.debug('----')
group_names.add(self.name)
for g in self.children:
g.debug_tree(group_names)
class MemHost(MemObject):
'''
In-memory representation of an inventory host.
'''
def __init__(self, name, port=None):
super(MemHost, self).__init__(name)
self.variables = {}
self.instance_id = None
self.name = name
if port:
# was `ansible_ssh_port` in older Ansible/Tower versions
self.variables['ansible_port'] = port
logger.debug('Loaded host: %s', self.name)
def __repr__(self):
return '<_in-memory-host_ `{}`>'.format(self.name)
class MemInventory(object):
'''
Common functions for an inventory loader from a given source.
'''
def __init__(self, all_group=None, group_filter_re=None, host_filter_re=None):
if all_group:
assert isinstance(all_group, MemGroup), '{} is not MemGroup instance'.format(all_group)
self.all_group = all_group
else:
self.all_group = self.create_group('all')
self.group_filter_re = group_filter_re
self.host_filter_re = host_filter_re
def create_host(self, host_name, port):
host = MemHost(host_name, port)
self.all_group.all_hosts[host_name] = host
return host
def get_host(self, name):
'''
Return a MemHost instance from host name, creating if needed. If name
contains brackets, they will NOT be interpreted as a host pattern.
'''
m = ipv6_port_re.match(name)
if m:
host_name = m.groups()[0]
port = int(m.groups()[1])
elif name.count(':') == 1:
host_name = name.split(':')[0]
try:
port = int(name.split(':')[1])
except (ValueError, UnicodeDecodeError):
logger.warning(u'Invalid port "%s" for host "%s"',
name.split(':')[1], host_name)
port = None
else:
host_name = name
port = None
if self.host_filter_re and not self.host_filter_re.match(host_name):
logger.debug('Filtering host %s', host_name)
return None
if host_name not in self.all_group.all_hosts:
self.create_host(host_name, port)
return self.all_group.all_hosts[host_name]
def create_group(self, group_name):
group = MemGroup(group_name)
if group_name not in ['all', 'ungrouped']:
self.all_group.all_groups[group_name] = group
return group
def get_group(self, name, all_group=None, child=False):
'''
Return a MemGroup instance from group name, creating if needed.
'''
all_group = all_group or self.all_group
if name in ['all', 'ungrouped']:
return all_group
if self.group_filter_re and not self.group_filter_re.match(name):
logger.debug('Filtering group %s', name)
return None
if name not in self.all_group.all_groups:
group = self.create_group(name)
if not child:
all_group.add_child_group(group)
return self.all_group.all_groups[name]
def delete_empty_groups(self):
for name, group in self.all_group.all_groups.items():
if not group.children and not group.hosts and not group.variables:
logger.debug('Removing empty group %s', name)
for parent in group.parents:
if group in parent.children:
parent.children.remove(group)
del self.all_group.all_groups[name]
# Conversion utilities
def mem_data_to_dict(inventory):
'''
Given an in-memory construct of an inventory, returns a dictionary that
follows Ansible guidelines on the structure of dynamic inventory sources
May be replaced by removing in-memory constructs within this file later
'''
all_group = inventory.all_group
inventory_data = OrderedDict([])
# Save hostvars to _meta
inventory_data['_meta'] = OrderedDict([])
hostvars = OrderedDict([])
for name, host_obj in all_group.all_hosts.items():
hostvars[name] = host_obj.variables
inventory_data['_meta']['hostvars'] = hostvars
# Save children of `all` group
inventory_data['all'] = OrderedDict([])
if all_group.variables:
inventory_data['all']['vars'] = all_group.variables
inventory_data['all']['children'] = [c.name for c in all_group.children]
inventory_data['all']['children'].append('ungrouped')
# Save details of declared groups individually
ungrouped_hosts = set(all_group.all_hosts.keys())
for name, group_obj in all_group.all_groups.items():
group_host_names = [h.name for h in group_obj.hosts]
group_children_names = [c.name for c in group_obj.children]
group_data = OrderedDict([])
if group_host_names:
group_data['hosts'] = group_host_names
ungrouped_hosts.difference_update(group_host_names)
if group_children_names:
group_data['children'] = group_children_names
if group_obj.variables:
group_data['vars'] = group_obj.variables
inventory_data[name] = group_data
# Save ungrouped hosts
inventory_data['ungrouped'] = OrderedDict([])
if ungrouped_hosts:
inventory_data['ungrouped']['hosts'] = list(ungrouped_hosts)
return inventory_data
def dict_to_mem_data(data, inventory=None):
'''
In-place operation on `inventory`, adds contents from `data` to the
in-memory representation of memory.
May be destructive on `data`
'''
assert isinstance(data, dict), 'Expected dict, received {}'.format(type(data))
if inventory is None:
inventory = MemInventory()
_meta = data.pop('_meta', {})
for k,v in data.iteritems():
group = inventory.get_group(k)
if not group:
continue
# Load group hosts/vars/children from a dictionary.
if isinstance(v, dict):
# Process hosts within a group.
hosts = v.get('hosts', {})
if isinstance(hosts, dict):
for hk, hv in hosts.iteritems():
host = inventory.get_host(hk)
if not host:
continue
if isinstance(hv, dict):
host.variables.update(hv)
else:
logger.warning('Expected dict of vars for '
'host "%s", got %s instead',
hk, str(type(hv)))
group.add_host(host)
elif isinstance(hosts, (list, tuple)):
for hk in hosts:
host = inventory.get_host(hk)
if not host:
continue
group.add_host(host)
else:
logger.warning('Expected dict or list of "hosts" for '
'group "%s", got %s instead', k,
str(type(hosts)))
# Process group variables.
vars = v.get('vars', {})
if isinstance(vars, dict):
group.variables.update(vars)
else:
logger.warning('Expected dict of vars for '
'group "%s", got %s instead',
k, str(type(vars)))
# Process child groups.
children = v.get('children', [])
if isinstance(children, (list, tuple)):
for c in children:
child = inventory.get_group(c, inventory.all_group, child=True)
if child and c != 'ungrouped':
group.add_child_group(child)
else:
logger.warning('Expected list of children for '
'group "%s", got %s instead',
k, str(type(children)))
# Load host names from a list.
elif isinstance(v, (list, tuple)):
for h in v:
host = inventory.get_host(h)
if not host:
continue
group.add_host(host)
else:
logger.warning('')
logger.warning('Expected dict or list for group "%s", '
'got %s instead', k, str(type(v)))
if k not in ['all', 'ungrouped']:
inventory.all_group.add_child_group(group)
if _meta:
for k,v in inventory.all_group.all_hosts.iteritems():
meta_hostvars = _meta['hostvars'].get(k, {})
if isinstance(meta_hostvars, dict):
v.variables.update(meta_hostvars)
else:
logger.warning('Expected dict of vars for '
'host "%s", got %s instead',
k, str(type(meta_hostvars)))
return inventory