From f614ef744fc2949ea8911683662ae50896a8f398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Andr=C3=A9?= Date: Thu, 28 Jul 2016 15:07:01 +0200 Subject: [PATCH] Add a dynamic ansible inventory script This commit adds an ansible dynamic inventory script that retrieve node information from heat and other OpenStack services and allows to connect to known nodes in the TripleO deployment. The script is strongly inspired by the dynamic inventory of the now retired tripleo-ansible project: https://github.com/stackforge/tripleo-ansible/blob/bd0346b55b586d591e6ca46aa42fc3002fa3dd45/plugins/inventory/heat.py Change-Id: I807fed12e9a42a71c130f393370ff4152831c27b --- README.rst | 36 +++-- requirements.txt | 4 + scripts/tripleo-ansible-inventory | 210 ++++++++++++++++++++++++++++++ setup.cfg | 3 + 4 files changed, 245 insertions(+), 8 deletions(-) create mode 100755 scripts/tripleo-ansible-inventory diff --git a/README.rst b/README.rst index b9731f925..70a713d76 100644 --- a/README.rst +++ b/README.rst @@ -151,11 +151,25 @@ information. Ansible Inventory ~~~~~~~~~~~~~~~~~ +Dynamic inventory ++++++++++++++++++ + +Tripleo-validations ships with a `dynamic inventory +`__, which +contacts the various OpenStack services to provide the addresses of the +deployed nodes as well as the undercloud. + +Just pass ``-i tripleo-ansible-inventory`` to ``ansible-playbook`` command:: + + ansible-playbook -i tripleo-ansible-inventory validations/hello_world.yaml + Hosts file ++++++++++ -The static inventory file lets you describe your environment. It should look -something like this:: +When more flexibility than what the current dynamic inventory provides is +needed or when running validations against a host that hasn't been deployed via +heat (such as the ``prep`` validations), it is possible to write a custom hosts +inventory file. It should look something like this:: [undercloud] undercloud.example.com @@ -236,7 +250,12 @@ Running the validations require ansible and a set of nodes to run them against. These nodes need to be reachable from the operator's machine and need to have an account it can ssh to and perform passwordless sudo. -The nodes need to be present in the static inventory file. +The nodes need to be present in the static inventory file or available from the +dynamic inventory script depending on which one the operator choses to use. +Check which nodes are available with:: + + $ source stackrc + $ tripleo-ansible-inventory --list In general, Ansible and the validations will be located on the *undercloud*, because it should have connectivity to all the *overcloud* nodes is already set @@ -245,7 +264,7 @@ up to SSH to them. :: $ source ~/stackrc - $ ansible-playbook -i hosts path/to/validation.yaml + $ ansible-playbook -i tripleo-ansible-inventory path/to/validation.yaml Example: Verify Undercloud RAM requirements ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -285,7 +304,7 @@ this under the same indentation as ``hosts`` and ``vars``:: When running it, it should output something like this:: - $ ansible-playbook -i hosts validations/undercloud-ram.yaml + $ ansible-playbook -i tripleo-ansible-inventory validations/undercloud-ram.yaml PLAY [undercloud] ************************************************************* @@ -303,7 +322,8 @@ When running it, it should output something like this:: Writing the full validation code is quite easy in this case because Ansible has done all the hard work for us already. We can use the ``ansible_memtotal_mb`` fact to get the amount of RAM (in megabytes) the tested server currently has. -For other useful values, run ``ansible -i hosts undercloud -m setup``. +For other useful values, run ``ansible -i tripleo-ansible-inventory +undercloud -m setup``. So, let's replace the hello world task with a real one:: @@ -367,11 +387,11 @@ Let's do that to test both success and failure cases. This should succeed but saying the RAM requirement is 1 GB:: - ansible-playbook -i hosts validations/undercloud-ram.yaml -e minimum_ram_gb=1 + ansible-playbook -i tripleo-ansible-inventory validations/undercloud-ram.yaml -e minimum_ram_gb=1 And this should fail by requiring much more RAM than is necessary:: - ansible-playbook -i hosts validations/undercloud-ram.yaml -e minimum_ram_gb=128 + ansible-playbook -i tripleo-ansible-inventory validations/undercloud-ram.yaml -e minimum_ram_gb=128 (the actual values may be different in your configuration -- just make sure one is low enough and the other too high) diff --git a/requirements.txt b/requirements.txt index 95d0fe88a..dcddca371 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,7 @@ # process, which may cause wedges in the gate later. pbr>=1.6 # Apache-2.0 +oslo.config>=3.14.0 # Apache-2.0 +python-heatclient>=1.1.0 # Apache-2.0 +python-keystoneclient!=2.1.0,>=2.0.0 # Apache-2.0 +python-novaclient!=2.33.0,>=2.29.0 # Apache-2.0 diff --git a/scripts/tripleo-ansible-inventory b/scripts/tripleo-ansible-inventory new file mode 100755 index 000000000..7c8672ced --- /dev/null +++ b/scripts/tripleo-ansible-inventory @@ -0,0 +1,210 @@ +#!/usr/bin/env python + +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# Copyright 2016 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. + +# TODO(mandre) +# If possible get info from ironic for hosts prior to deployment + +from __future__ import print_function + +import json +import os +import sys + +from heatclient.v1 import client as heat_client +from keystoneclient.v3 import client as keystone_client +from novaclient import client as nova_client +from oslo_config import cfg + +opts = [ + cfg.StrOpt('host', help='List details about the specific host'), + cfg.BoolOpt('list', help='List active hosts'), + cfg.StrOpt('username', default=os.environ.get('OS_USERNAME')), + cfg.StrOpt('password', default=os.environ.get('OS_PASSWORD')), + cfg.StrOpt('auth-url', default=os.environ.get('OS_AUTH_URL')), + cfg.StrOpt('auth-token', default=os.environ.get('OS_AUTH_TOKEN')), + cfg.StrOpt('project-name', default=os.environ.get('OS_TENANT_NAME')), + cfg.StrOpt('plan', default=os.environ.get('TRIPLEO_PLAN_NAME')), +] + + +def _parse_config(): + default_config = os.environ.get('TRIPLEO_INVENTORY_CONFIG') + if default_config: + default_config = [default_config] + + configs = cfg.ConfigOpts() + configs.register_cli_opts(opts) + configs(prog='tripleo-ansible-inventory', + default_config_files=default_config) + if configs.auth_url is None: + print('ERROR: auth-url not defined and OS_AUTH_URL environment ' + 'variable missing, unable to proceed.', file=sys.stderr) + sys.exit(1) + if '/v2.0' in configs.auth_url: + configs.auth_url = configs.auth_url.replace('/v2.0', '/v3') + if not configs.plan: + configs.plan = 'overcloud' + return configs + + +class TripleoInventory(object): + def __init__(self, configs): + self.configs = configs + self._ksclient = None + self._hclient = None + self._nclient = None + + def fetch_stack_resources(self, resource_name): + heatclient = self.hclient + novaclient = self.nclient + stack = self.configs.plan + ret = [] + try: + resource_id = heatclient.resources.get(stack, resource_name) \ + .physical_resource_id + for resource in heatclient.resources.list(resource_id): + node = heatclient.resources.get(resource_id, + resource.resource_name) + node_resource = node.attributes['nova_server_resource'] + nova_server = novaclient.servers.get(node_resource) + if nova_server.status == 'ACTIVE': + ret.append(nova_server.networks['ctlplane'][0]) + except Exception: + # Ignore non existent stacks or resources + pass + return ret + + def get_overcloud_output(self, output_name): + try: + stack = self.hclient.stacks.get(self.configs.plan) + for output in stack.outputs: + if output['output_key'] == output_name: + return output['output_value'] + except Exception: + return None + + def list(self): + ret = { + 'undercloud': { + 'hosts': ['localhost'], + 'vars': { + 'ansible_connection': 'local', + 'ansible_become': True, + }, + } + } + + public_vip = self.get_overcloud_output('PublicVip') + if public_vip: + ret['undercloud']['vars']['public_vip'] = public_vip + + controller_group = self.fetch_stack_resources('Controller') + if controller_group: + ret['controller'] = controller_group + + compute_group = self.fetch_stack_resources('Compute') + if compute_group: + ret['compute'] = compute_group + + if any([controller_group, compute_group]): + ret['overcloud'] = { + 'children': list(set(ret.keys()) - set(['undercloud'])), + 'vars': { + # TODO(mandre) retrieve SSH user from heat + 'ansible_ssh_user': 'heat-admin', + 'ansible_become': True, + } + } + + print(json.dumps(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 + print(json.dumps({})) + + @property + def ksclient(self): + if self._ksclient is None: + try: + if self.configs.auth_token: + self._ksclient = keystone_client.Client( + auth_url=self.configs.auth_url, + username=self.configs.username, + token=self.configs.auth_token, + project_name=self.configs.project_name) + else: + self._ksclient = keystone_client.Client( + auth_url=self.configs.auth_url, + username=self.configs.username, + password=self.configs.password, + project_name=self.configs.project_name) + self._ksclient.authenticate() + except Exception as e: + print("Error connecting to Keystone: {}".format(e.message), + file=sys.stderr) + sys.exit(1) + return self._ksclient + + @property + def hclient(self): + if self._hclient is None: + ksclient = self.ksclient + endpoint = ksclient.service_catalog.url_for( + service_type='orchestration', endpoint_type='publicURL') + try: + self._hclient = heat_client.Client( + endpoint=endpoint, + token=ksclient.auth_token) + except Exception as e: + print("Error connecting to Heat: {}".format(e.message), + file=sys.stderr) + sys.exit(1) + return self._hclient + + @property + def nclient(self): + if self._nclient is None: + ksclient = self.ksclient + endpoint = ksclient.service_catalog.url_for( + service_type='compute', endpoint_type='publicURL') + try: + self._nclient = nova_client.Client( + '2', + bypass_url=endpoint, + auth_token=ksclient.auth_token) + except Exception as e: + print("Error connecting to Nova: {}".format(e.message), + file=sys.stderr) + sys.exit(1) + return self._nclient + + +def main(): + configs = _parse_config() + inventory = TripleoInventory(configs) + if configs.list: + inventory.list() + elif configs.host: + inventory.host() + sys.exit(0) + +if __name__ == '__main__': + main() diff --git a/setup.cfg b/setup.cfg index b5a753c38..3e0c763c2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,9 @@ classifier = packages = tripleo_validations +scripts = + scripts/tripleo-ansible-inventory + data_files = share/openstack-tripleo-validations/ = hosts.sample share/openstack-tripleo-validations/validations = validations/*