From aeaa57176571b4f9613c2495a2fd822bf0661c2f Mon Sep 17 00:00:00 2001 From: John Fulton Date: Fri, 30 Aug 2019 22:05:55 +0000 Subject: [PATCH] Introduce TripleoInventories object This object can be constructed by passing it a mapping of stack name to TripleoInventory objects. It has a merge method which merges all of the TripleoInventory objects into one OrderedDict which appends the stack name and an underscore to each host group to avoid name collisions. It has a write_static_inventory method which writes the merged OrderedDict into an Ansible inventory. Change-Id: I522f1acbd39ad36ae1fec896188d3e20e0575566 Implements: blueprint split-control-plane-unified-inventory --- tripleo_common/inventories.py | 105 ++++++ tripleo_common/tests/test_inventories.py | 418 +++++++++++++++++++++++ 2 files changed, 523 insertions(+) create mode 100644 tripleo_common/inventories.py create mode 100644 tripleo_common/tests/test_inventories.py diff --git a/tripleo_common/inventories.py b/tripleo_common/inventories.py new file mode 100644 index 000000000..f40468d3b --- /dev/null +++ b/tripleo_common/inventories.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# Copyright 2019 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 +import os.path +import yaml + + +class TemplateDumper(yaml.SafeDumper): + def represent_ordered_dict(self, data): + return self.represent_dict(data.items()) + + +TemplateDumper.add_representer(OrderedDict, + TemplateDumper.represent_ordered_dict) + + +class TripleoInventories(object): + def __init__(self, stack_to_inv_obj_map): + """ + Input: a mapping of stack->TripleoInventory objects, e.g. + stack_to_inv_obj_map['central'] = TripleoInventory('central') + stack_to_inv_obj_map['edge0'] = TripleoInventory('edge0') + """ + self.stack_to_inv_obj_map = stack_to_inv_obj_map + self.inventory = OrderedDict() + + def merge(self): + """Merge TripleoInventory objects into self.inventory""" + for stack, inv_obj in self.stack_to_inv_obj_map.items(): + # convert each inventory object into an ordered dict + inv = inv_obj.list() + # only want one undercloud, shouldn't matter which + if 'Undercloud' not in self.inventory.keys(): + self.inventory['Undercloud'] = inv['Undercloud'] + self.inventory['Undercloud']['hosts'] = {'undercloud': {}} + # add 'plans' to create a list to append to + self.inventory['Undercloud']['vars']['plans'] = [] + + # save the plan for this stack in the plans list + plan = inv['Undercloud']['vars']['plan'] + self.inventory['Undercloud']['vars']['plans'].append(plan) + + for key in inv.keys(): + if key != 'Undercloud': + new_key = stack + '_' + key + if 'children' in inv[key].keys(): + roles = [] + for child in inv[key]['children']: + roles.append(stack + '_' + child) + self.inventory[new_key] = {} + self.inventory[new_key]['vars'] = inv[key]['vars'] + for role in roles: + self.inventory[new_key]['children'] = {} + self.inventory[new_key]['children'][role] = {} + if key == 'overcloud': + # useful to have just stack name refer to children + self.inventory[stack] = self.inventory[new_key] + else: + if key != '_meta': + self.inventory[new_key] = inv[key] + self.inventory[new_key]['hosts'] = {} + self.inventory[new_key]['hosts'] = \ + inv['_meta']['hostvars'] + + # 'plan' doesn't make sense when using multiple plans + self.inventory['Undercloud']['vars']['plan'] = '' + # sort plans list for consistency + self.inventory['Undercloud']['vars']['plans'].sort() + + def write_static_inventory(self, inventory_file_path, extra_vars=None): + """Convert OrderedDict inventory to static yaml format in a file.""" + allowed_extensions = ('.yaml', '.yml', '.json') + if not os.path.splitext(inventory_file_path)[1] in allowed_extensions: + raise ValueError("Path %s does not end with one of %s extensions" + % (inventory_file_path, + ",".join(allowed_extensions))) + + if extra_vars: + for var, value in extra_vars.items(): + if var in self.inventory: + self.inventory[var]['vars'].update(value) + + with open(inventory_file_path, 'w') as inventory_file: + yaml.dump(self.inventory, inventory_file, TemplateDumper) + + def host(self): + # 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 {} diff --git a/tripleo_common/tests/test_inventories.py b/tripleo_common/tests/test_inventories.py new file mode 100644 index 000000000..47d083ba8 --- /dev/null +++ b/tripleo_common/tests/test_inventories.py @@ -0,0 +1,418 @@ +# -*- 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. + +import fixtures +import os +import yaml + +from mock import MagicMock +from tripleo_common.tests import base +from tripleo_common.inventories import TripleoInventories + + +class TestInventories(base.TestCase): + def setUp(self): + super(TestInventories, self).setUp() + mock_inv_central = MagicMock() + mock_inv_edge0 = MagicMock() + mock_inv_central.list.return_value = self._mock_inv_central_data() + mock_inv_edge0.list.return_value = self._mock_inv_edge0_data() + self.stack_to_inv_obj_map = { + 'central': mock_inv_central, + 'edge0': mock_inv_edge0 + } + self.inventories = TripleoInventories(self.stack_to_inv_obj_map) + + def test_merge(self): + self.inventories.merge() + expected = self._mock_inv_merged_data() + for k in expected.keys(): + self.assertEqual(expected[k], self.inventories.inventory[k]) + + def test_inventory_write_static(self): + self.inventories.merge() + tmp_dir = self.useFixture(fixtures.TempDir()).path + inv_path = os.path.join(tmp_dir, "inventory.yaml") + self.inventories.write_static_inventory(inv_path) + expected = self._mock_inv_merged_data() + with open(inv_path, 'r') as f: + loaded_inv = yaml.safe_load(f) + self.assertEqual(expected, loaded_inv) + + def _mock_inv_central_data(self): + return { + "Undercloud": { + "hosts": [ + "undercloud" + ], + "vars": { + "username": "admin", + "overcloud_keystone_url": "http://192.168.24.21:5000", + "project_name": "admin", + "overcloud_horizon_url": "http://192.168.24.21/dashboard", + "auth_url": "https://192.168.24.2:13000", + "ansible_connection": "local", + "cacert": "/etc/pki/ca-trust/cm-local-ca.pem", + "ansible_host": "localhost", + "ansible_remote_tmp": "/tmp/ansible-${USER}", + "undercloud_service_list": [ + "tripleo_nova_compute", + "tripleo_heat_engine", + "tripleo_ironic_conductor", + "tripleo_swift_container_server", + "tripleo_swift_object_server", + "tripleo_mistral_engine" + ], + "ansible_python_interpreter": "/usr/bin/python", + "overcloud_admin_password": "7uCCDn4lIKQ4i7ONsPdgX1KbC", + "plan": "central" + } + }, + "Controller": { + "hosts": [ + "central-controller-0" + ], + "vars": { + "tripleo_role_name": "Controller", + "serial": "1", + "ansible_ssh_user": "heat-admin", + } + }, + "overcloud": { + "children": [ + "Controller" + ], + "vars": { + "storage_mgmt_vip": "192.168.24.21", + "container_cli": "podman", + "ctlplane_vip": "192.168.24.21", + "redis_vip": "192.168.24.11", + "internal_api_vip": "192.168.24.21", + "external_vip": "192.168.24.21", + "storage_vip": "192.168.24.21" + } + }, + "kernel": { + "children": [ + "Controller" + ], + "vars": { + "ansible_ssh_user": "heat-admin" + } + }, + "ovn_controller": { + "children": [ + "Controller" + ], + "vars": { + "ansible_ssh_user": "heat-admin" + } + }, + "_meta": { + "hostvars": { + "central-controller-0": { + "storage_ip": "192.168.24.12", + "storage_mgmt_ip": "192.168.24.12", + "external_ip": "192.168.24.12", + "ctlplane_ip": "192.168.24.12", + "tenant_ip": "192.168.24.12", + "internal_api_ip": "192.168.24.12", + "management_ip": "192.168.24.12", + "enabled_networks": [ + "management", + "storage", + "ctlplane", + "external", + "internal_api", + "storage_mgmt", + "tenant" + ], + "ansible_host": "192.168.24.12" + } + } + } + } + + def _mock_inv_edge0_data(self): + return { + "Undercloud": { + "hosts": [ + "undercloud" + ], + "vars": { + "username": "admin", + "overcloud_keystone_url": "http://192.168.24.21:5000", + "project_name": "admin", + "overcloud_horizon_url": "http://192.168.24.21/dashboard", + "auth_url": "https://192.168.24.2:13000", + "ansible_connection": "local", + "cacert": "/etc/pki/ca-trust/cm-local-ca.pem", + "ansible_host": "localhost", + "ansible_remote_tmp": "/tmp/ansible-${USER}", + "undercloud_service_list": [ + "tripleo_nova_compute", + "tripleo_heat_engine", + "tripleo_ironic_conductor", + "tripleo_swift_container_server", + "tripleo_swift_object_server", + "tripleo_mistral_engine" + ], + "ansible_python_interpreter": "/usr/bin/python", + "overcloud_admin_password": "7uCCDn4lIKQ4i7ONsPdgX1KbC", + "plan": "edge0" + } + }, + "DistributedComputeHCI": { + "hosts": [ + "edge0-distributedcomputehci-0" + ], + "vars": { + "tripleo_role_name": "DistributedComputeHCI", + "serial": "1", + "ansible_ssh_user": "heat-admin", + } + }, + "overcloud": { + "children": [ + "DistributedComputeHCI" + ], + "vars": { + "storage_mgmt_vip": "192.168.24.20", + "container_cli": "podman", + "ctlplane_vip": "192.168.24.20", + "redis_vip": "192.168.24.24", + "internal_api_vip": "192.168.24.20", + "external_vip": "192.168.24.20", + "storage_vip": "192.168.24.20" + } + }, + "kernel": { + "children": [ + "DistributedComputeHCI" + ], + "vars": { + "ansible_ssh_user": "heat-admin" + } + }, + "ovn_controller": { + "children": [ + "DistributedComputeHCI" + ], + "vars": { + "ansible_ssh_user": "heat-admin" + } + }, + "_meta": { + "hostvars": { + "edge0-distributedcomputehci-0": { + "storage_ip": "192.168.24.13", + "storage_mgmt_ip": "192.168.24.13", + "external_ip": "192.168.24.13", + "ctlplane_ip": "192.168.24.13", + "tenant_ip": "192.168.24.13", + "internal_api_ip": "192.168.24.13", + "management_ip": "192.168.24.13", + "enabled_networks": [ + "management", + "storage", + "ctlplane", + "external", + "internal_api", + "storage_mgmt", + "tenant" + ], + "ansible_host": "192.168.24.13" + } + } + } + } + + def _mock_inv_merged_data(self): + return { + "Undercloud": { + "hosts": { + "undercloud": {} + }, + "vars": { + "username": "admin", + "overcloud_keystone_url": "http://192.168.24.21:5000", + "project_name": "admin", + "overcloud_horizon_url": "http://192.168.24.21/dashboard", + "auth_url": "https://192.168.24.2:13000", + "ansible_connection": "local", + "cacert": "/etc/pki/ca-trust/cm-local-ca.pem", + "ansible_host": "localhost", + "ansible_remote_tmp": "/tmp/ansible-${USER}", + "undercloud_service_list": [ + "tripleo_nova_compute", + "tripleo_heat_engine", + "tripleo_ironic_conductor", + "tripleo_swift_container_server", + "tripleo_swift_object_server", + "tripleo_mistral_engine" + ], + "ansible_python_interpreter": "/usr/bin/python", + "overcloud_admin_password": "7uCCDn4lIKQ4i7ONsPdgX1KbC", + "plan": '', + "plans": [ + "central", + "edge0" + ] + } + }, + "central_Controller": { + "hosts": { + "central-controller-0": { + "storage_ip": "192.168.24.12", + "storage_mgmt_ip": "192.168.24.12", + "external_ip": "192.168.24.12", + "ctlplane_ip": "192.168.24.12", + "tenant_ip": "192.168.24.12", + "internal_api_ip": "192.168.24.12", + "management_ip": "192.168.24.12", + "enabled_networks": [ + "management", + "storage", + "ctlplane", + "external", + "internal_api", + "storage_mgmt", + "tenant" + ], + "ansible_host": "192.168.24.12" + } + }, + "vars": { + "tripleo_role_name": "Controller", + "serial": "1", + "ansible_ssh_user": "heat-admin", + } + }, + "central_overcloud": { + "vars": { + "storage_mgmt_vip": "192.168.24.21", + "container_cli": "podman", + "ctlplane_vip": "192.168.24.21", + "redis_vip": "192.168.24.11", + "internal_api_vip": "192.168.24.21", + "external_vip": "192.168.24.21", + "storage_vip": "192.168.24.21" + }, + "children": { + "central_Controller": {} + } + }, + "central": { + "vars": { + "storage_mgmt_vip": "192.168.24.21", + "container_cli": "podman", + "ctlplane_vip": "192.168.24.21", + "redis_vip": "192.168.24.11", + "internal_api_vip": "192.168.24.21", + "external_vip": "192.168.24.21", + "storage_vip": "192.168.24.21" + }, + "children": { + "central_Controller": {} + } + }, + "central_kernel": { + "vars": { + "ansible_ssh_user": "heat-admin" + }, + "children": { + "central_Controller": {} + } + }, + "central_ovn_controller": { + "vars": { + "ansible_ssh_user": "heat-admin" + }, + "children": { + "central_Controller": {} + } + }, + "edge0_DistributedComputeHCI": { + "hosts": { + "edge0-distributedcomputehci-0": { + "storage_ip": "192.168.24.13", + "storage_mgmt_ip": "192.168.24.13", + "external_ip": "192.168.24.13", + "ctlplane_ip": "192.168.24.13", + "tenant_ip": "192.168.24.13", + "internal_api_ip": "192.168.24.13", + "management_ip": "192.168.24.13", + "enabled_networks": [ + "management", + "storage", + "ctlplane", + "external", + "internal_api", + "storage_mgmt", + "tenant" + ], + "ansible_host": "192.168.24.13" + } + }, + "vars": { + "tripleo_role_name": "DistributedComputeHCI", + "serial": "1", + "ansible_ssh_user": "heat-admin", + } + }, + "edge0_overcloud": { + "vars": { + "storage_mgmt_vip": "192.168.24.20", + "container_cli": "podman", + "ctlplane_vip": "192.168.24.20", + "redis_vip": "192.168.24.24", + "internal_api_vip": "192.168.24.20", + "external_vip": "192.168.24.20", + "storage_vip": "192.168.24.20" + }, + "children": { + "edge0_DistributedComputeHCI": {} + } + }, + "edge0": { + "vars": { + "storage_mgmt_vip": "192.168.24.20", + "container_cli": "podman", + "ctlplane_vip": "192.168.24.20", + "redis_vip": "192.168.24.24", + "internal_api_vip": "192.168.24.20", + "external_vip": "192.168.24.20", + "storage_vip": "192.168.24.20" + }, + "children": { + "edge0_DistributedComputeHCI": {} + } + }, + "edge0_kernel": { + "vars": { + "ansible_ssh_user": "heat-admin" + }, + "children": { + "edge0_DistributedComputeHCI": {} + } + }, + "edge0_ovn_controller": { + "vars": { + "ansible_ssh_user": "heat-admin" + }, + "children": { + "edge0_DistributedComputeHCI": {} + } + } + }