ea6954f72f
Change Idb7780f55e4a1fd77dd76becbf67c1ccbf220db7 restructured the python inventory generation code so that it would be possible to install it with pip. This change removes (most) of the import path hacks and switches to using a pip-installed version of osa_toolkit. Of note, the path hacks are left in place for the dynamic_inventory.py file for now, as that file is really more of an endpoint, but is tested. Also, the bootstrap-ansible.sh script was modified to install the code; this is unnecessary with the tox environments because the tox directive 'usedevelop=True' does that already. Production environments still need this, though. Finally, to maintain usability when called directly, the interpreter for dynamic_inventory.py was updated from `/usr/bin/env` to `/opt/ansible-runime/python`. This change ensures that in a full deployment the user is using the exact same code paths whether Ansible invokes the script, or it is called directly. This also means that using the script locally on a development machine, it must be invoked as an argument to Python, unless the ansible-runtime directory exists. Change-Id: Iafa573b1b144f98528d5e0aceb3f36e9de2a22a2
316 lines
10 KiB
Python
316 lines
10 KiB
Python
# Copyright 2014, Rackspace US, Inc.
|
|
#
|
|
# 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.
|
|
#
|
|
# (c) 2014, Kevin Carter <kevin.carter@rackspace.com>
|
|
# (c) 2015, Major Hayden <major@mhtx.net>
|
|
#
|
|
|
|
import copy
|
|
import datetime
|
|
import json
|
|
import logging
|
|
import os
|
|
from osa_toolkit import dictutils as du
|
|
import tarfile
|
|
import yaml
|
|
|
|
|
|
logger = logging.getLogger('osa-inventory')
|
|
|
|
INVENTORY_FILENAME = 'openstack_inventory.json'
|
|
|
|
|
|
class MissingDataSource(Exception):
|
|
def __init__(self, *sources):
|
|
self.sources = sources
|
|
|
|
error_msg = "Could not read data sources: '{sources}'."
|
|
self.message = error_msg.format(sources=self.sources)
|
|
|
|
def __str__(self):
|
|
return self.message
|
|
|
|
def __repr__(self):
|
|
return self.message
|
|
|
|
|
|
def _get_search_paths(preferred_path=None, suffix=None):
|
|
"""Return a list of search paths, including the standard location
|
|
|
|
:param preferred_path: A search path to prefer to a standard location
|
|
:param suffix: Appended to the search paths, e.g. subdirectory or filename
|
|
:return: ``(list)`` Path strings to search
|
|
"""
|
|
|
|
search_paths = [
|
|
os.path.join(
|
|
'/etc', 'openstack_deploy'
|
|
),
|
|
]
|
|
if preferred_path is not None:
|
|
search_paths.insert(0, os.path.expanduser(preferred_path))
|
|
|
|
if suffix:
|
|
search_paths = [os.path.join(p, suffix) for p in search_paths]
|
|
|
|
return search_paths
|
|
|
|
|
|
def file_find(filename, preferred_path=None, raise_if_missing=True):
|
|
"""Return the path to an existing file, or False if no file is found.
|
|
|
|
If no file is found and raise_if_missing is True, MissingDataSource
|
|
will be raised.
|
|
|
|
The file lookup will be done in the following directories:
|
|
* ``preferred_path`` [Optional]
|
|
* ``/etc/openstack_deploy/``
|
|
|
|
:param filename: ``str`` Name of the file to find
|
|
:param preferred_path: ``str`` Additional directory to look in FIRST
|
|
:param raise_if_missing: ``bool`` Should a MissingDataSource be raised if
|
|
the file is not found
|
|
"""
|
|
|
|
search_paths = _get_search_paths(preferred_path, suffix=filename)
|
|
|
|
for file_candidate in search_paths:
|
|
if os.path.isfile(file_candidate):
|
|
return file_candidate
|
|
|
|
# The file was not found
|
|
if raise_if_missing:
|
|
raise MissingDataSource(search_paths)
|
|
else:
|
|
return False
|
|
|
|
|
|
def dir_find(preferred_path=None, suffix=None, raise_if_missing=True):
|
|
"""Return the path to the user configuration files.
|
|
|
|
If no directory is found the system will exit.
|
|
|
|
The lookup will be done in the following directories:
|
|
|
|
* ``preferred_path`` [Optional]
|
|
* ``/etc/openstack_deploy/``
|
|
|
|
:param preferred_path: ``str`` Additional directory to look in FIRST
|
|
:param suffix: ``str`` Name of a subdirectory to find under standard paths
|
|
:param raise_if_missing: ``bool`` Should a MissingDataSource be raised if
|
|
the directory is not found.
|
|
"""
|
|
search_paths = _get_search_paths(preferred_path, suffix)
|
|
|
|
for f in search_paths:
|
|
if os.path.isdir(f):
|
|
return f
|
|
|
|
# The directory was not found
|
|
if raise_if_missing:
|
|
raise MissingDataSource(search_paths)
|
|
else:
|
|
return False
|
|
|
|
|
|
def _extra_config(user_defined_config, base_dir):
|
|
"""Discover new items in any extra directories and add the new values.
|
|
|
|
:param user_defined_config: ``dict``
|
|
:param base_dir: ``str``
|
|
"""
|
|
for root_dir, _, files in os.walk(base_dir):
|
|
for name in files:
|
|
if name.endswith(('.yml', '.yaml')):
|
|
with open(os.path.join(root_dir, name), 'rb') as f:
|
|
du.merge_dict(
|
|
user_defined_config,
|
|
yaml.safe_load(f.read()) or {}
|
|
)
|
|
logger.debug("Merged overrides from file {}".format(name))
|
|
|
|
|
|
def _make_backup(backup_path, source_file_path):
|
|
"""Create a backup of all previous inventory files as a tar archive
|
|
|
|
:param backup_path: where to store the backup file
|
|
:param source_file_path: path of file to backup
|
|
:return:
|
|
"""
|
|
|
|
inventory_backup_file = os.path.join(
|
|
backup_path,
|
|
'backup_openstack_inventory.tar'
|
|
)
|
|
with tarfile.open(inventory_backup_file, 'a') as tar:
|
|
basename = os.path.basename(source_file_path)
|
|
backup_name = _get_backup_name(basename)
|
|
tar.add(source_file_path, arcname=backup_name)
|
|
logger.debug("Backup written to {}".format(inventory_backup_file))
|
|
|
|
|
|
def _get_backup_name(basename):
|
|
"""Return a name for a backup file based on the time
|
|
|
|
:param basename: serves as prefix for the return value
|
|
:return: a name for a backup file based on current time
|
|
"""
|
|
|
|
utctime = datetime.datetime.utcnow()
|
|
utctime = utctime.strftime("%Y%m%d_%H%M%S")
|
|
return '{}-{}.json'.format(basename, utctime)
|
|
|
|
|
|
def write_hostnames(save_path, hostnames_ips):
|
|
"""Write a list of all hosts and their given IP addresses
|
|
|
|
NOTE: the file is saved in json format to a file with the name
|
|
``openstack_hostnames_ips.yml``
|
|
|
|
:param save_path: path to save the file to, will use default location if
|
|
None or an invalid path is provided
|
|
:param hostnames_ips: the list of all hosts and their IP addresses
|
|
"""
|
|
|
|
file_path = dir_find(save_path)
|
|
hostnames_ip_file = os.path.join(file_path, 'openstack_hostnames_ips.yml')
|
|
|
|
with open(hostnames_ip_file, 'wb') as f:
|
|
f.write(
|
|
json.dumps(
|
|
hostnames_ips,
|
|
indent=4,
|
|
separators=(',', ': '),
|
|
sort_keys=True
|
|
).encode('ascii')
|
|
)
|
|
|
|
|
|
def _load_from_json(filename, preferred_path=None, raise_if_missing=True):
|
|
"""Return a dictionary found in json format in a given file
|
|
|
|
:param filename: ``str`` Name of the file to read from
|
|
:param preferred_path: ``str`` Path to the json file to try FIRST
|
|
:param raise_if_missing: ``bool`` Should a MissingDataSource be raised if
|
|
the file is not found
|
|
:return ``(dict, str)`` Dictionary describing the JSON file contents or
|
|
False, and the fully resolved file name loaded or None
|
|
"""
|
|
|
|
target_file = file_find(filename, preferred_path, raise_if_missing)
|
|
dictionary = False
|
|
if target_file is not False:
|
|
with open(target_file, 'rb') as f_handle:
|
|
dictionary = json.loads(f_handle.read().decode('ascii'))
|
|
|
|
return dictionary, target_file
|
|
|
|
|
|
def load_inventory(preferred_path=None, default_inv=None, filename=None):
|
|
"""Create an inventory dictionary from the given source file or a default
|
|
inventory. If an inventory is found then a backup tarball is created
|
|
as well.
|
|
|
|
:param preferred_path: ``str`` Path to the inventory directory to try FIRST
|
|
:param default_inv: ``dict`` Default inventory skeleton
|
|
|
|
:return: ``(dict, str)`` Dictionary describing the JSON file contents or
|
|
``default_inv``, and the directory from which the inventory was loaded
|
|
or should have been loaded from.
|
|
"""
|
|
|
|
if filename:
|
|
inv_fn = filename
|
|
else:
|
|
inv_fn = INVENTORY_FILENAME
|
|
|
|
inventory, file_loaded = _load_from_json(inv_fn, preferred_path,
|
|
raise_if_missing=False)
|
|
if file_loaded is not False:
|
|
load_path = os.path.dirname(file_loaded)
|
|
else:
|
|
load_path = dir_find(preferred_path)
|
|
|
|
if inventory is not False:
|
|
logger.debug("Loaded existing inventory from {}".format(file_loaded))
|
|
_make_backup(load_path, file_loaded)
|
|
else:
|
|
logger.debug("No existing inventory, created fresh skeleton.")
|
|
inventory = copy.deepcopy(default_inv)
|
|
|
|
return inventory, load_path
|
|
|
|
|
|
def save_inventory(inventory_json, save_path):
|
|
"""Save an inventory dictionary
|
|
|
|
:param inventory_json: ``str`` String of JSON formatted inventory to store
|
|
:param save_path: ``str`` Path of the directory to save to
|
|
"""
|
|
|
|
if INVENTORY_FILENAME == save_path:
|
|
inventory_file = file_find(save_path)
|
|
else:
|
|
inventory_file = os.path.join(save_path, INVENTORY_FILENAME)
|
|
with open(inventory_file, 'wb') as f:
|
|
f.write(inventory_json.encode('ascii'))
|
|
logger.info("Inventory written")
|
|
|
|
|
|
def load_environment(config_path, environment):
|
|
"""Create an environment dictionary from config files
|
|
|
|
:param config_path: ``str`` path where the environment files are kept
|
|
:param environment: ``dict`` dictionary to populate with environment data
|
|
"""
|
|
|
|
# Load all YAML files found in the env.d directory
|
|
env_plugins = dir_find(config_path, 'env.d', raise_if_missing=False)
|
|
|
|
if env_plugins is not False:
|
|
_extra_config(user_defined_config=environment, base_dir=env_plugins)
|
|
logger.debug("Loaded environment from {}".format(config_path))
|
|
|
|
return environment
|
|
|
|
|
|
def load_user_configuration(config_path=None):
|
|
"""Create a user configuration dictionary from config files
|
|
|
|
:param config_path: ``str`` path where the configuration files are kept
|
|
"""
|
|
|
|
user_defined_config = dict()
|
|
|
|
# Load the user defined configuration file
|
|
user_config_file = file_find('openstack_user_config.yml',
|
|
preferred_path=config_path,
|
|
raise_if_missing=False)
|
|
if user_config_file is not False:
|
|
with open(user_config_file, 'rb') as f:
|
|
user_defined_config.update(yaml.safe_load(f.read()) or {})
|
|
|
|
# Load anything in a conf.d directory if found
|
|
base_dir = dir_find(config_path, 'conf.d', raise_if_missing=False)
|
|
if base_dir is not False:
|
|
_extra_config(user_defined_config, base_dir)
|
|
|
|
# Exit if no user_config was found and loaded
|
|
if not user_defined_config:
|
|
raise MissingDataSource(_get_search_paths(config_path) +
|
|
_get_search_paths(config_path, 'conf.d'))
|
|
|
|
logger.debug("User configuration loaded from: {}".format(user_config_file))
|
|
return user_defined_config
|