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:

bd0346b55b/plugins/inventory/heat.py

Change-Id: I807fed12e9a42a71c130f393370ff4152831c27b
This commit is contained in:
Martin André 2016-07-28 15:07:01 +02:00 committed by Tomas Sedovic
parent 236ce74c37
commit f614ef744f
4 changed files with 245 additions and 8 deletions

View File

@ -151,11 +151,25 @@ information.
Ansible Inventory
~~~~~~~~~~~~~~~~~
Dynamic inventory
+++++++++++++++++
Tripleo-validations ships with a `dynamic inventory
<http://docs.ansible.com/ansible/intro_dynamic_inventory.html>`__, 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)

View File

@ -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

210
scripts/tripleo-ansible-inventory Executable file
View File

@ -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()

View File

@ -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/*