diff --git a/lib/ansible/module_utils/openstack.py b/lib/ansible/module_utils/openstack.py index 64f95437143..5c4503f94ce 100644 --- a/lib/ansible/module_utils/openstack.py +++ b/lib/ansible/module_utils/openstack.py @@ -30,6 +30,9 @@ import os def openstack_argument_spec(): + # DEPRECATED: This argument spec is only used for the deprecated old + # OpenStack modules. It turns out that modern OpenStack auth is WAY + # more complex than this. # Consume standard OpenStack environment variables. # This is mainly only useful for ad-hoc command line operation as # in playbooks one would assume variables would be used appropriately @@ -67,3 +70,40 @@ def openstack_find_nova_addresses(addresses, ext_tag, key_name=None): ret.append(interface_spec['addr']) return ret +def openstack_full_argument_spec(**kwargs): + spec = dict( + cloud=dict(default=None), + auth_plugin=dict(default=None), + auth=dict(default=None), + auth_token=dict(default=None), + region_name=dict(default=None), + availability_zone=dict(default=None), + state=dict(default='present', choices=['absent', 'present']), + wait=dict(default=True, type='bool'), + timeout=dict(default=180, type='int'), + endpoint_type=dict( + default='publicURL', choices=['publicURL', 'internalURL'] + ) + ) + spec.update(kwargs) + return spec + + +def openstack_module_kwargs(**kwargs): + ret = dict( + required_one_of=[ + ['cloud', 'auth'], + ], + mutually_exclusive=[ + ['auth', 'auth_token'], + ['auth_plugin', 'auth_token'], + ], + ) + for key in ('mutually_exclusive', 'required_together', 'required_one_of'): + if key in kwargs: + if key in ret: + ret[key].extend(kwargs[key]) + else: + ret[key] = kwargs[key] + + return ret diff --git a/lib/ansible/utils/module_docs_fragments/openstack.py b/lib/ansible/utils/module_docs_fragments/openstack.py new file mode 100644 index 00000000000..d740bc719c3 --- /dev/null +++ b/lib/ansible/utils/module_docs_fragments/openstack.py @@ -0,0 +1,88 @@ +# Copyright (c) 2014 Hewlett-Packard Development Company, L.P. +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +class ModuleDocFragment(object): + + # Standard openstack documentation fragment + DOCUMENTATION = ''' +options: + cloud: + description: + - Named cloud to operate against. Provides default values for I(auth) and I(auth_plugin) + required: false + auth: + description: + - Dictionary containing auth information as needed by the cloud's auth + plugin strategy. For the default I{password) plugin, this would contain + I(auth_url), I(username), I(password), I(project_name) and any + information about domains if the cloud supports them. For other plugins, + this param will need to contain whatever parameters that auth plugin + requires. This parameter is not needed if a named cloud is provided. + required: false + auth_plugin: + description: + - Name of the auth plugin to use. If the cloud uses something other than + password authentication, the name of the plugin should be indicated here + and the contents of the I(auth) parameter should be updated accordingly. + required: false + default: password + auth_token: + description: + - An auth token obtained previously. If I(auth_token) is given, + I(auth) and I(auth_plugin) are not needed. + region_name: + description: + - Name of the region. + required: false + availability_zone: + description: + - Name of the availability zone. + required: false + state: + description: + - Should the resource be present or absent. + choices: [present, absent] + default: present + wait: + description: + - Should ansible wait until the requested resource is complete. + required: false + default: "yes" + choices: ["yes", "no"] + timeout: + description: + - How long should ansible wait for the requested resource. + required: false + default: 180 + endpoint_type: + description: + - Endpoint URL type to fetch from the service catalog. + choices: [publicURL, internalURL] + required: false + default: publicURL +requirements: + - shade +notes: + - The standard OpenStack environment variables, such as C(OS_USERNAME) + may be user instead of providing explicit values. + - Auth information is driven by os-client-config, which means that values + can come from a yaml config file in /etc/ansible/openstack.yaml, + /etc/openstack/clouds.yaml or ~/.config/openstack/clouds.yaml, then from + standard environment variables, then finally by explicit parameters in + plays. +''' diff --git a/plugins/inventory/openstack.py b/plugins/inventory/openstack.py new file mode 100755 index 00000000000..c49d3c1fc43 --- /dev/null +++ b/plugins/inventory/openstack.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python + +# Copyright (c) 2012, Marco Vito Moscaritolo +# Copyright (c) 2013, Jesse Keating +# Copyright (c) 2014, Hewlett-Packard Development Company, L.P. +# +# This module is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This software is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this software. If not, see . + +import argparse +import collections +import os +import sys +import time + +try: + import json +except: + import simplejson as json + +import os_client_config +import shade + + +class OpenStackInventory(object): + + def __init__(self, private=False, refresh=False): + self.openstack_config = os_client_config.config.OpenStackConfig( + os_client_config.config.CONFIG_FILES.append( + '/etc/ansible/openstack.yml'), + private) + self.clouds = shade.openstack_clouds(self.openstack_config) + self.refresh = refresh + + self.cache_max_age = self.openstack_config.get_cache_max_age() + cache_path = self.openstack_config.get_cache_path() + + # Cache related + if not os.path.exists(cache_path): + os.makedirs(cache_path) + self.cache_file = os.path.join(cache_path, "ansible-inventory.cache") + + def is_cache_stale(self): + ''' Determines if cache file has expired, or if it is still valid ''' + if os.path.isfile(self.cache_file): + mod_time = os.path.getmtime(self.cache_file) + current_time = time.time() + if (mod_time + self.cache_max_age) > current_time: + return False + return True + + def get_host_groups(self): + if self.refresh or self.is_cache_stale(): + groups = self.get_host_groups_from_cloud() + self.write_cache(groups) + else: + return json.load(open(self.cache_file, 'r')) + return groups + + def write_cache(self, groups): + with open(self.cache_file, 'w') as cache_file: + cache_file.write(self.json_format_dict(groups)) + + def get_host_groups_from_cloud(self): + groups = collections.defaultdict(list) + hostvars = collections.defaultdict(dict) + + for cloud in self.clouds: + + # Cycle on servers + for server in cloud.list_servers(): + + meta = cloud.get_server_meta(server) + + if 'interface_ip' not in meta['server_vars']: + # skip this host if it doesn't have a network address + continue + + server_vars = meta['server_vars'] + hostvars[server.name][ + 'ansible_ssh_host'] = server_vars['interface_ip'] + hostvars[server.name]['openstack'] = server_vars + + for group in meta['groups']: + groups[group].append(server.name) + + if hostvars: + groups['_meta'] = {'hostvars': hostvars} + return groups + + def json_format_dict(self, data): + return json.dumps(data, sort_keys=True, indent=2) + + def list_instances(self): + groups = self.get_host_groups() + # Return server list + print(self.json_format_dict(groups)) + + def get_host(self, hostname): + groups = self.get_host_groups() + hostvars = groups['_meta']['hostvars'] + if hostname in hostvars: + print(self.json_format_dict(hostvars[hostname])) + + +def parse_args(): + parser = argparse.ArgumentParser(description='OpenStack Inventory Module') + parser.add_argument('--private', + action='store_true', + help='Use private address for ansible host') + parser.add_argument('--refresh', action='store_true', + help='Refresh cached information') + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument('--list', action='store_true', + help='List active servers') + group.add_argument('--host', help='List details about the specific host') + return parser.parse_args() + + +def main(): + args = parse_args() + try: + inventory = OpenStackInventory(args.private, args.refresh) + if args.list: + inventory.list_instances() + elif args.host: + inventory.get_host(args.host) + except shade.OpenStackCloudException as e: + print(e.message) + sys.exit(1) + sys.exit(0) + + +if __name__ == '__main__': + main()