Merge "Inital import of tripleo ansible inventory code"

This commit is contained in:
Zuul 2017-12-20 05:03:38 +00:00 committed by Gerrit Code Review
commit f4c212cfa0
2 changed files with 522 additions and 0 deletions

228
tripleo_common/inventory.py Normal file
View File

@ -0,0 +1,228 @@
#!/usr/bin/env python
# Copyright 2017 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from collections import OrderedDict
from heatclient.exc import HTTPNotFound
HOST_NETWORK = 'ctlplane'
class StackOutputs(object):
"""Item getter for stack outputs.
It takes a long time to resolve stack outputs. This class ensures that
we only have to do it once and then reuse the results from that call in
subsequent lookups. It also lazy loads the outputs so we don't spend time
on unnecessary Heat calls.
"""
def __init__(self, plan, hclient):
self.plan = plan
self.outputs = {}
self.hclient = hclient
self.stack = None
def _load_outputs(self):
"""Load outputs from the stack if necessary
Retrieves the stack outputs if that has not already happened. If it
has then this is a noop.
Sets the outputs to an empty dict if the stack is not found.
"""
if not self.stack:
try:
self.stack = self.hclient.stacks.get(self.plan)
except HTTPNotFound:
self.outputs = {}
return
self.outputs = {i['output_key']: i['output_value']
for i in self.stack.outputs
}
def __getitem__(self, key):
self._load_outputs()
return self.outputs[key]
def __iter__(self):
self._load_outputs()
return iter(self.outputs.keys())
def get(self, key, default=None):
try:
self.__getitem__(key)
except KeyError:
pass
return self.outputs.get(key, default)
class TripleoInventory(object):
def __init__(self, configs, session, hclient):
self.configs = configs
self.session = session
self.hclient = hclient
self.stack_outputs = StackOutputs(self.configs.plan, self.hclient)
@staticmethod
def get_roles_by_service(enabled_services):
# Flatten the lists of services for each role into a set
services = set(
[item for role_services in enabled_services.values()
for item in role_services])
roles_by_services = {}
for service in services:
roles_by_services[service] = []
for role, val in enabled_services.items():
if service in val:
roles_by_services[service].append(role)
roles_by_services[service] = sorted(roles_by_services[service])
return roles_by_services
def get_overcloud_environment(self):
try:
environment = self.hclient.stacks.environment(self.configs.plan)
return environment
except HTTPNotFound:
return {}
UNDERCLOUD_SERVICES = [
'openstack-nova-compute', 'openstack-heat-engine',
'openstack-ironic-conductor', 'openstack-swift-container',
'openstack-swift-object', 'openstack-mistral-engine']
def get_undercloud_service_list(self):
"""Return list of undercloud services - currently static
Replace this when we have a better way - e.g. heat deploys undercloud
"""
return self.UNDERCLOUD_SERVICES
def list(self):
ret = OrderedDict({
'undercloud': {
'hosts': ['localhost'],
'vars': {
'ansible_connection': 'local',
'auth_url': self.configs.auth_url,
'cacert': self.configs.cacert,
'os_auth_token': self.session.get_token(),
'plan': self.configs.plan,
'project_name': self.configs.project_name,
'username': self.configs.username,
},
}
})
swift_url = self.session.get_endpoint(service_type='object-store',
interface='public')
if swift_url:
ret['undercloud']['vars']['undercloud_swift_url'] = swift_url
keystone_url = self.stack_outputs.get('KeystoneURL')
if keystone_url:
ret['undercloud']['vars']['overcloud_keystone_url'] = keystone_url
admin_password = self.get_overcloud_environment().get(
'parameter_defaults', {}).get('AdminPassword')
if admin_password:
ret['undercloud']['vars']['overcloud_admin_password'] =\
admin_password
endpoint_map = self.stack_outputs.get('EndpointMap')
ret['undercloud']['vars']['undercloud_service_list'] = \
self.get_undercloud_service_list()
if endpoint_map:
horizon_endpoint = endpoint_map.get('HorizonPublic', {}).get('uri')
if horizon_endpoint:
ret['undercloud']['vars']['overcloud_horizon_url'] =\
horizon_endpoint
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(HOST_NETWORK) or []
shortnames = [n.split(".%s." % HOST_NETWORK)[0] for n in names]
# Create a group per hostname to map hostname to IP
ips = role_net_ip_map[role][HOST_NETWORK]
for idx, name in enumerate(shortnames):
ret[name] = {'hosts': [ips[idx]]}
if 'server_ids' in role_node_id_map:
ret[name]['vars'] = {
'deploy_server_id': role_node_id_map[
'server_ids'][role][idx]}
# Add variable for listing enabled networks in the node
ret[name]['vars']['enabled_networks'] = \
[str(net) for net in role_net_ip_map[role]]
# Add variable for IP on each network
for net in role_net_ip_map[role]:
ret[name]['vars']["%s_ip" % net] = \
role_net_ip_map[role][net][idx]
networks.update(ret[name]['vars']['enabled_networks'])
children.append(role)
ret[role] = {
'children': sorted(shortnames),
'vars': {
'ansible_ssh_user': self.configs.ansible_ssh_user,
'bootstrap_server_id': role_node_id_map.get(
'bootstrap_server_id'),
'role_name': role,
}
}
if children:
vip_map = self.stack_outputs.get('VipMap', {})
vips = {(vip_name + "_vip"): vip
for vip_name, vip in vip_map.items()
if vip and (vip_name in networks or vip_name == 'redis')}
ret['overcloud'] = {
'children': sorted(children),
'vars': vips
}
# Associate services with roles
roles_by_service = self.get_roles_by_service(
self.stack_outputs.get('EnabledServices', {}))
for service, roles in roles_by_service.items():
service_children = [role for role in roles
if ret.get(role) is not None]
if service_children:
ret[service.lower()] = {
'children': service_children,
'vars': {
'ansible_ssh_user': self.configs.ansible_ssh_user
}
}
# Prevent Ansible from repeatedly calling us to get empty host details
ret['_meta'] = {'hostvars': {}}
return ret
def host(self):
# NOTE(mandre)
# Dynamic inventory scripts must return empty json if they don't
# provide detailed info for hosts:
# http://docs.ansible.com/ansible/developing_inventory.html
return {}

View File

@ -0,0 +1,294 @@
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from heatclient.exc import HTTPNotFound
from mock import MagicMock
from tripleo_common.inventory import StackOutputs
from tripleo_common.inventory import TripleoInventory
from tripleo_common.tests import base
MOCK_ENABLED_SERVICES = {
"ObjectStorage": [
"kernel",
"swift_storage",
"tripleo_packages"
],
"Controller": [
"kernel",
"keystone",
"tripleo_packages"
],
"Compute": [
"nova_compute",
"kernel",
"tripleo_packages"
],
"CephStorage": [
"kernel",
"tripleo_packages"
],
"BlockStorage": [
"cinder_volume",
"kernel",
"tripleo_packages"
]
}
class TestInventory(base.TestCase):
def setUp(self):
super(TestInventory, self).setUp()
self.outputs_data = {'outputs': [
{'output_key': 'EnabledServices',
'output_value': {
'Controller': ['a', 'b', 'c'],
'Compute': ['d', 'e', 'f'],
'CustomRole': ['g', 'h', 'i']}},
{'output_key': 'KeystoneURL',
'output_value': 'xyz://keystone'},
{'output_key': 'ServerIdData',
'output_value': {
'server_ids': {
'Controller': ['a', 'b', 'c'],
'Compute': ['d'],
'CustomRole': ['e']},
'bootstrap_server_id': 'a'}},
{'output_key': 'RoleNetHostnameMap',
'output_value': {
'Controller': {
'ctlplane': ['c-0.ctlplane.localdomain',
'c-1.ctlplane.localdomain',
'c-2.ctlplane.localdomain']},
'Compute': {
'ctlplane': ['cp-0.ctlplane.localdomain']},
'CustomRole': {
'ctlplane': ['cs-0.ctlplane.localdomain']}}},
{'output_key': 'RoleNetIpMap',
'output_value': {
'Controller': {
'ctlplane': ['x.x.x.1',
'x.x.x.2',
'x.x.x.3']},
'Compute': {
'ctlplane': ['y.y.y.1']},
'CustomRole': {
'ctlplane': ['z.z.z.1']}}},
{'output_key': 'VipMap',
'output_value': {
'ctlplane': 'x.x.x.4',
'redis': 'x.x.x.6'}}]}
self.plan_name = 'overcloud'
self.hclient = MagicMock()
self.hclient.stacks.environment.return_value = {
'parameter_defaults': {'AdminPassword': 'theadminpw'}}
self.mock_stack = MagicMock()
self.mock_stack.outputs = self.outputs_data['outputs']
self.hclient.stacks.get.return_value = self.mock_stack
self.configs = MagicMock()
self.configs.plan = self.plan_name
self.configs.auth_url = 'xyz://keystone.local'
self.configs.cacert = 'acacert'
self.configs.project_name = 'admin'
self.configs.username = 'admin'
self.configs.ansible_ssh_user = 'heat-admin'
self.session = MagicMock()
self.session.get_token.return_value = 'atoken'
self.session.get_endpoint.return_value = 'anendpoint'
self.outputs = StackOutputs('overcloud', self.hclient)
self.inventory = TripleoInventory(
self.configs, self.session, self.hclient)
self.inventory.stack_outputs = self.outputs
def test_get_roles_by_service(self):
services = TripleoInventory.get_roles_by_service(
MOCK_ENABLED_SERVICES)
expected = {
'kernel': ['BlockStorage', 'CephStorage', 'Compute', 'Controller',
'ObjectStorage'],
'swift_storage': ['ObjectStorage'],
'tripleo_packages': ['BlockStorage', 'CephStorage', 'Compute',
'Controller', 'ObjectStorage'],
'keystone': ['Controller'],
'nova_compute': ['Compute'],
'cinder_volume': ['BlockStorage'],
}
self.assertDictEqual(services, expected)
def test_outputs_are_empty_if_stack_doesnt_exist(self):
self.hclient.stacks.get.side_effect = HTTPNotFound('not found')
stack_outputs = StackOutputs('no-plan', self.hclient)
self.assertEqual(list(stack_outputs), [])
def test_outputs_valid_key_calls_api(self):
expected = 'xyz://keystone'
self.hclient.stacks.output_show.return_value = dict(output=dict(
output_value=expected))
self.assertEqual(expected, self.outputs['KeystoneURL'])
# This should also support the get method
self.assertEqual(expected, self.outputs.get('KeystoneURL'))
self.assertTrue(self.hclient.called_once_with('overcloud',
'KeystoneURL'))
def test_outputs_invalid_key_raises_keyerror(self):
self.assertRaises(KeyError, lambda: self.outputs['Invalid'])
def test_outputs_get_method_returns_default(self):
default = 'default value'
self.assertEqual(default, self.outputs.get('Invalid', default))
def test_outputs_iterating_returns_list_of_output_keys(self):
self.assertEqual(
{'EnabledServices', 'KeystoneURL', 'ServerIdData',
'RoleNetHostnameMap', 'RoleNetIpMap', 'VipMap'},
set([o for o in self.outputs]))
def test_inventory_list(self):
expected = {'c-0': {'hosts': ['x.x.x.1'],
'vars': {'deploy_server_id': 'a',
'ctlplane_ip': 'x.x.x.1',
'enabled_networks': ['ctlplane']}},
'c-1': {'hosts': ['x.x.x.2'],
'vars': {'deploy_server_id': 'b',
'ctlplane_ip': 'x.x.x.2',
'enabled_networks': ['ctlplane']}},
'c-2': {'hosts': ['x.x.x.3'],
'vars': {'deploy_server_id': 'c',
'ctlplane_ip': 'x.x.x.3',
'enabled_networks': ['ctlplane']}},
'Compute': {
'children': ['cp-0'],
'vars': {'ansible_ssh_user': 'heat-admin',
'bootstrap_server_id': 'a',
'role_name': 'Compute'}},
'Controller': {
'children': ['c-0', 'c-1', 'c-2'],
'vars': {'ansible_ssh_user': 'heat-admin',
'bootstrap_server_id': 'a',
'role_name': 'Controller'}},
'cp-0': {'hosts': ['y.y.y.1'],
'vars': {'deploy_server_id': 'd',
'ctlplane_ip': 'y.y.y.1',
'enabled_networks': ['ctlplane']}},
'cs-0': {'hosts': ['z.z.z.1'],
'vars': {'deploy_server_id': 'e',
'ctlplane_ip': 'z.z.z.1',
'enabled_networks': ['ctlplane']}},
'CustomRole': {
'children': ['cs-0'],
'vars': {'ansible_ssh_user': 'heat-admin',
'bootstrap_server_id': 'a',
'role_name': 'CustomRole'}},
'overcloud': {
'children': ['Compute', 'Controller', 'CustomRole'],
'vars': {
'ctlplane_vip': 'x.x.x.4',
'redis_vip': 'x.x.x.6'}},
'undercloud': {
'hosts': ['localhost'],
'vars': {'ansible_connection': 'local',
'auth_url': 'xyz://keystone.local',
'cacert': 'acacert',
'os_auth_token': 'atoken',
'overcloud_keystone_url': 'xyz://keystone',
'overcloud_admin_password': 'theadminpw',
'plan': 'overcloud',
'project_name': 'admin',
'undercloud_service_list': [
'openstack-nova-compute',
'openstack-heat-engine',
'openstack-ironic-conductor',
'openstack-swift-container',
'openstack-swift-object',
'openstack-mistral-engine'],
'undercloud_swift_url': 'anendpoint',
'username': 'admin'}}}
inv_list = self.inventory.list()
for k in expected:
self.assertEqual(expected[k], inv_list[k])
def test_ansible_ssh_user(self):
self.configs.ansible_ssh_user = 'my-custom-admin'
self.inventory = TripleoInventory(
self.configs, self.session, self.hclient)
self.inventory.stack_outputs = self.outputs
expected = {'c-0': {'hosts': ['x.x.x.1'],
'vars': {'deploy_server_id': 'a',
'ctlplane_ip': 'x.x.x.1',
'enabled_networks': ['ctlplane']}},
'c-1': {'hosts': ['x.x.x.2'],
'vars': {'deploy_server_id': 'b',
'ctlplane_ip': 'x.x.x.2',
'enabled_networks': ['ctlplane']}},
'c-2': {'hosts': ['x.x.x.3'],
'vars': {'deploy_server_id': 'c',
'ctlplane_ip': 'x.x.x.3',
'enabled_networks': ['ctlplane']}},
'Compute': {
'children': ['cp-0'],
'vars': {'ansible_ssh_user': 'my-custom-admin',
'bootstrap_server_id': 'a',
'role_name': 'Compute'}},
'Controller': {
'children': ['c-0', 'c-1', 'c-2'],
'vars': {'ansible_ssh_user': 'my-custom-admin',
'bootstrap_server_id': 'a',
'role_name': 'Controller'}},
'cp-0': {'hosts': ['y.y.y.1'],
'vars': {'deploy_server_id': 'd',
'ctlplane_ip': 'y.y.y.1',
'enabled_networks': ['ctlplane']}},
'cs-0': {'hosts': ['z.z.z.1'],
'vars': {'deploy_server_id': 'e',
'ctlplane_ip': 'z.z.z.1',
'enabled_networks': ['ctlplane']}},
'CustomRole': {
'children': ['cs-0'],
'vars': {'ansible_ssh_user': 'my-custom-admin',
'bootstrap_server_id': 'a',
'role_name': 'CustomRole'}},
'overcloud': {
'children': ['Compute', 'Controller', 'CustomRole'],
'vars': {
'ctlplane_vip': 'x.x.x.4',
'redis_vip': 'x.x.x.6'}},
'undercloud': {
'hosts': ['localhost'],
'vars': {'ansible_connection': 'local',
'auth_url': 'xyz://keystone.local',
'cacert': 'acacert',
'os_auth_token': 'atoken',
'overcloud_keystone_url': 'xyz://keystone',
'overcloud_admin_password': 'theadminpw',
'plan': 'overcloud',
'project_name': 'admin',
'undercloud_service_list': [
'openstack-nova-compute',
'openstack-heat-engine',
'openstack-ironic-conductor',
'openstack-swift-container',
'openstack-swift-object',
'openstack-mistral-engine'],
'undercloud_swift_url': 'anendpoint',
'username': 'admin'}}}
inv_list = self.inventory.list()
for k in expected:
self.assertEqual(expected[k], inv_list[k])