Re-factor inventory.py use setdefault pervasively

This refactors the inventory code to pervasively
use 'setdefault' to assign values to all inventory
keys.

The idea is to use multiple sources in order, and
build up the inventory enriching it with more data as
it becomes available.

With the pervasive 'setdefault' usage all values in
the inventory will reflect the value first discovered.

The follow up change
  I9e1851aeb92c9e257fd18fdc500ee5c01b1eb53e
adds support to use neutron as a source of data for
the inventory in addition to the heat stack.

Discovery order will be neutron then heat.

Partial-Implements: blueprint network-data-v2-ports
Change-Id: Ia00ef50d6272fd1b3dd6dbbf7ff6edd2472d7dd0
This commit is contained in:
Harald Jensås 2020-12-07 01:04:56 +01:00
parent acf9eb95d7
commit 131ff2b4b8
3 changed files with 153 additions and 147 deletions

View File

@ -58,7 +58,7 @@ class TripleoInventories(object):
inventory['Undercloud']['vars']['plans'] = []
# save the plan for this stack in the plans list
plan = inv['Undercloud']['vars']['plan']
plan = inv['Undercloud']['vars'].get('plan')
if plan is not None:
inventory['Undercloud']['vars']['plans'].append(plan)

View File

@ -91,6 +91,7 @@ class TripleoInventory(object):
host_network=None, ansible_python_interpreter=None,
undercloud_connection=UNDERCLOUD_CONNECTION_LOCAL,
undercloud_key_file=None, serial=1):
self.session = session
self.hclient = hclient
self.host_network = host_network or HOST_NETWORK
self.auth_url = auth_url
@ -158,162 +159,103 @@ class TripleoInventory(object):
return stack
def list(self, dynamic=True):
ret = OrderedDict({
'Undercloud': {
'hosts': self._hosts(['undercloud'], dynamic),
'vars': {
'ansible_host': 'localhost',
'ansible_python_interpreter': sys.executable,
'ansible_connection': self.undercloud_connection,
# see https://github.com/ansible/ansible/issues/41808
'ansible_remote_tmp': '/tmp/ansible-${USER}',
'auth_url': self.auth_url,
'plan': None,
'project_name': self.project_name,
'username': self.username,
},
}
})
if self.cacert:
ret['Undercloud']['vars']['cacert'] = \
self.cacert
if self.ansible_python_interpreter:
ret['Undercloud']['vars']['ansible_python_interpreter'] = \
self.ansible_python_interpreter
if self.undercloud_connection == UNDERCLOUD_CONNECTION_SSH:
ret['Undercloud']['vars']['ansible_ssh_user'] = \
self.ansible_ssh_user
if self.undercloud_key_file:
ret['Undercloud']['vars']['ansible_ssh_private_key_file'] = \
self.undercloud_key_file
ret['Undercloud']['vars']['undercloud_service_list'] = \
self.get_undercloud_service_list()
if dynamic:
# Prevent Ansible from repeatedly calling us to get empty host
# details
ret['_meta'] = {'hostvars': self.hostvars}
self.stack = self._get_stack()
def _inventory_from_heat_outputs(self, ret, children, dynamic):
if not self.stack:
return ret
ret['Undercloud']['vars']['plan'] = self.plan_name
admin_password = self.get_overcloud_environment().get(
'parameter_defaults', {}).get('AdminPassword')
if admin_password:
ret['Undercloud']['vars']['overcloud_admin_password'] =\
admin_password
self.stack_outputs = StackOutputs(self.stack)
keystone_url = self.stack_outputs.get('KeystoneURL')
if keystone_url:
ret['Undercloud']['vars']['overcloud_keystone_url'] = keystone_url
endpoint_map = self.stack_outputs.get('EndpointMap')
if endpoint_map:
horizon_endpoint = endpoint_map.get('HorizonPublic', {}).get('uri')
if horizon_endpoint:
ret['Undercloud']['vars']['overcloud_horizon_url'] =\
horizon_endpoint
return
vip_map = self.stack_outputs.get('VipMap', {})
role_net_ip_map = self.stack_outputs.get('RoleNetIpMap', {})
role_node_id_map = self.stack_outputs.get('ServerIdData', {})
networks = set()
role_net_hostname_map = self.stack_outputs.get(
'RoleNetHostnameMap', {})
children = []
for role, hostnames in role_net_hostname_map.items():
if hostnames:
names = hostnames.get(self.host_network) or []
shortnames = [n.split(".%s." % self.host_network)[0].lower()
for n in names]
ips = role_net_ip_map[role][self.host_network]
if not ips:
raise Exception("No IPs found for %s role on %s network"
% (role, self.host_network))
hosts = {}
for idx, name in enumerate(shortnames):
hosts[name] = {}
hosts[name].update({
'ansible_host': ips[idx]})
if 'server_ids' in role_node_id_map:
hosts[name].update({
'deploy_server_id': role_node_id_map[
'server_ids'][role][idx]})
# Add variable for IP on each network
for net in role_net_ip_map[role]:
hosts[name].update({
"%s_ip" % net:
role_net_ip_map[role][net][idx]})
# Add variables for hostname on each network
for net in role_net_hostname_map[role]:
hosts[name].update({
"%s_hostname" % net:
role_net_hostname_map[role][net][idx]})
for role_name, hostnames in role_net_hostname_map.items():
if not hostnames:
continue
children.append(role)
net_ip_map = role_net_ip_map[role_name]
ips = net_ip_map[self.host_network]
if not ips:
raise Exception("No IPs found for %s role on %s network" %
(role_name, self.host_network))
if not dynamic:
hosts_format = hosts
else:
hosts_format = [h for h in hosts.keys()]
hosts_format.sort()
net_hostname_map = role_net_hostname_map[role_name]
bootstrap_server_id = role_node_id_map.get('bootstrap_server_id')
node_id_map = role_node_id_map.get('server_ids')
if node_id_map:
srv_id_map = node_id_map.get(role_name)
role_networks = sorted([
str(net) for net in role_net_ip_map[role]
])
networks.update(role_networks)
role_networks = sorted([str(net) for net in net_ip_map])
networks.update(role_networks)
ret[role] = {
'hosts': hosts_format,
'vars': {
'ansible_ssh_user': self.ansible_ssh_user,
'bootstrap_server_id': role_node_id_map.get(
'bootstrap_server_id'),
'tripleo_role_name': role,
'tripleo_role_networks': role_networks,
'serial': self.serial,
}
role = ret.setdefault(role_name, {})
hosts = role.setdefault('hosts', {})
role_vars = role.setdefault('vars', {})
}
role_vars.setdefault('ansible_ssh_user', self.ansible_ssh_user)
role_vars.setdefault('bootstrap_server_id', bootstrap_server_id)
role_vars.setdefault('tripleo_role_name', role_name)
role_vars.setdefault('tripleo_role_networks', role_networks)
role_vars.setdefault('serial', self.serial)
if self.ansible_python_interpreter:
ret[role]['vars']['ansible_python_interpreter'] = \
self.ansible_python_interpreter
if self.ansible_python_interpreter:
role_vars.setdefault('ansible_python_interpreter',
self.ansible_python_interpreter)
self.hostvars.update(hosts)
names = hostnames.get(self.host_network) or []
shortnames = [n.split(".%s." % self.host_network)[0].lower()
for n in names]
for idx, name in enumerate(shortnames):
host = hosts.setdefault(name, {})
host.setdefault('ansible_host', ips[idx])
if srv_id_map:
host.setdefault('deploy_server_id', srv_id_map[idx])
# Add variable for IP on each network
for net in net_ip_map:
host.setdefault('{}_ip'.format(net), net_ip_map[net][idx])
# Add variables for hostname on each network
for net in net_hostname_map:
host.setdefault(
'{}_hostname'.format(net), net_hostname_map[net][idx])
children.add(role_name)
self.hostvars.update(hosts)
if dynamic:
hosts_format = [h for h in hosts.keys()]
hosts_format.sort()
ret[role_name]['hosts'] = hosts_format
if children:
vip_map = self.stack_outputs.get('VipMap', {})
overcloud_vars = {
(vip_name + "_vip"): vip for vip_name, vip in vip_map.items()
if vip and (vip_name in networks or vip_name == 'redis')
}
allovercloud = ret.setdefault('allovercloud', {})
overcloud_vars = allovercloud.setdefault('vars', {})
overcloud_vars['container_cli'] = \
self.get_overcloud_environment().get(
'parameter_defaults', {}).get('ContainerCli')
for vip_name, vip in vip_map.items():
if vip and (vip_name in networks or vip_name == 'redis'):
overcloud_vars.setdefault('{}_vip'.format(vip_name), vip)
ret['allovercloud'] = {
'children': self._hosts(sorted(children), dynamic),
'vars': overcloud_vars
}
overcloud_vars.setdefault(
'container_cli', self.get_overcloud_environment().get(
'parameter_defaults', {}).get('ContainerCli'))
allovercloud.setdefault('children', self._hosts(sorted(children),
dynamic))
ret.setdefault(
self.plan_name, {'children': self._hosts(['allovercloud'],
dynamic)})
ret[self.plan_name] = {
'children': self._hosts(['allovercloud'], dynamic)
}
if self.plan_name != 'overcloud':
ret['overcloud'] = {
'children': self._hosts(['allovercloud'], dynamic),
'deprecated': 'Deprecated by allovercloud group in Ussuri'
}
ret.setdefault('overcloud',
{'children': self._hosts(['allovercloud'],
dynamic),
'deprecated': ('Deprecated by allovercloud '
'group in Ussuri')})
# Associate services with roles
roles_by_service = self.get_roles_by_service(
@ -344,16 +286,81 @@ class TripleoInventory(object):
service_children = [role for role in roles
if ret.get(role) is not None]
if service_children:
svc_host = service.lower()
ret[svc_host] = {
'children': self._hosts(service_children, dynamic),
'vars': {
'ansible_ssh_user': self.ansible_ssh_user
}
}
svc_host = ret.setdefault(service.lower(), {})
svc_host_vars = svc_host.setdefault('vars', {})
svc_host.setdefault('children', self._hosts(service_children,
dynamic))
svc_host_vars.setdefault('ansible_ssh_user',
self.ansible_ssh_user)
if self.ansible_python_interpreter:
ret[svc_host]['vars']['ansible_python_interpreter'] = \
self.ansible_python_interpreter
svc_host_vars.setdefault('ansible_python_interpreter',
self.ansible_python_interpreter)
def _undercloud_inventory(self, ret, dynamic):
undercloud = ret.setdefault('Undercloud', {})
undercloud.setdefault('hosts', self._hosts(['undercloud'], dynamic))
_vars = undercloud.setdefault('vars', {})
_vars.setdefault('ansible_host', 'localhost')
_vars.setdefault('ansible_connection', self.undercloud_connection)
# see https://github.com/ansible/ansible/issues/41808
_vars.setdefault('ansible_remote_tmp', '/tmp/ansible-${USER}')
_vars.setdefault('auth_url', self.auth_url)
_vars.setdefault('project_name', self.project_name)
_vars.setdefault('username', self.username)
if self.cacert:
_vars['cacert'] = self.cacert
if self.ansible_python_interpreter:
_vars.setdefault('ansible_python_interpreter',
self.ansible_python_interpreter)
else:
_vars.setdefault('ansible_python_interpreter', sys.executable)
if self.undercloud_connection == UNDERCLOUD_CONNECTION_SSH:
_vars.setdefault('ansible_ssh_user', self.ansible_ssh_user)
if self.undercloud_key_file:
_vars.setdefault('ansible_ssh_private_key_file',
self.undercloud_key_file)
_vars.setdefault('undercloud_service_list',
self.get_undercloud_service_list())
# Remaining variables need the stack to be resolved ...
if not self.stack:
return
_vars.setdefault('plan', self.plan_name)
admin_password = self.get_overcloud_environment().get(
'parameter_defaults', {}).get('AdminPassword')
if admin_password:
_vars.setdefault('overcloud_admin_password', admin_password)
keystone_url = self.stack_outputs.get('KeystoneURL')
if keystone_url:
_vars.setdefault('overcloud_keystone_url', keystone_url)
endpoint_map = self.stack_outputs.get('EndpointMap')
if endpoint_map:
horizon_endpoint = endpoint_map.get('HorizonPublic', {}).get('uri')
if horizon_endpoint:
_vars.setdefault('overcloud_horizon_url', horizon_endpoint)
def list(self, dynamic=True):
ret = OrderedDict()
if dynamic:
# Prevent Ansible from repeatedly calling us to get empty host
# details
ret.setdefault('_meta', {'hostvars': self.hostvars})
children = set()
self.stack = self._get_stack()
self.stack_outputs = StackOutputs(self.stack)
self._undercloud_inventory(ret, dynamic)
self._inventory_from_heat_outputs(ret, children, dynamic)
return ret

View File

@ -323,7 +323,6 @@ class TestInventory(base.TestCase):
'auth_url': 'xyz://keystone.local',
'cacert': 'acacert',
'project_name': 'admin',
'plan': None,
'undercloud_service_list': [
'tripleo_nova_compute',
'tripleo_heat_engine',