summaryrefslogtreecommitdiff
path: root/lib/ansible/plugins/inventory/openstack.py
blob: 6c9bae5fbe90a3749bc9cee07e6d518c49d32a70 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
# Copyright (c) 2012, Marco Vito Moscaritolo <marco@agavee.com>
# Copyright (c) 2013, Jesse Keating <jesse.keating@rackspace.com>
# Copyright (c) 2015, Hewlett-Packard Development Company, L.P.
# Copyright (c) 2016, Rackspace Australia
# Copyright (c) 2017, Red Hat, Inc.
#
# 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 <http://www.gnu.org/licenses/>.
'''
DOCUMENTATION:
    name: openstack
    plugin_type: inventory
    short_description: OpenStack inventory source
    description:
        - Get inventory hosts from OpenStack clouds
        - Uses openstack.(yml|yaml) YAML configuration file to configure the inventory plugin
        - Uses standard clouds.yaml YAML configuration file to configure cloud credentials
    options:
        show_all:
            description: toggles showing all vms vs only those with a working IP
            type: boolean
            default: False
        inventory_hostname:
            description: |
                What to register as the inventory hostname.
                If set to 'uuid' the uuid of the server will be used and a
                group will be created for the server name.
                If set to 'name' the name of the server will be used unless
                there are more than one server with the same name in which
                case the 'uuid' logic will be used.
                Default is to do 'name', which is the opposite of the old
                openstack.py inventory script's option use_hostnames)
            type: string
            choices:
                - name
                - uuid
            default: "name"
        expand_hostvars:
            description: |
                Run extra commands on each host to fill in additional
                information about the host. May interrogate cinder and
                neutron and can be expensive for people with many hosts.
                (Note, the default value of this is opposite from the default
                old openstack.py inventory script's option expand_hostvars)
            type: boolean
            default: False
        private:
            description: |
                Use the private interface of each server, if it has one, as
                the host's IP in the inventory. This can be useful if you are
                running ansible inside a server in the cloud and would rather
                communicate to your servers over the private network.
            type: boolean
            default: False
        only_clouds:
            description: |
                List of clouds from clouds.yaml to use, instead of using
                the whole list.
            type: list
            default: []
        fail_on_errors:
            description: |
                Causes the inventory to fail and return no hosts if one cloud
                has failed (for example, bad credentials or being offline).
                When set to False, the inventory will return as many hosts as
                it can from as many clouds as it can contact. (Note, the
                default value of this is opposite from the old openstack.py
                inventory script's option fail_on_errors)
            type: boolean
            default: False
        clouds_yaml_path:
            description: |
                Override path to clouds.yaml file. If this value is given it
                will be searched first. The default path for the
                ansible inventory adds /etc/ansible/openstack.yaml and
                /etc/ansible/openstack.yml to the regular locations documented
                at https://docs.openstack.org/os-client-config/latest/user/configuration.html#config-files
            type: string
            default: None
        compose:
            description: Create vars from jinja2 expressions.
            type: dictionary
            default: {}
        groups:
            description: Add hosts to group based on Jinja2 conditionals.
            type: dictionary
            default: {}
EXAMPLES:
# file must be named openstack.yaml or openstack.yml
# Make the plugin behave like the default behavior of the old script
simple_config_file:
    plugin: openstack
    inventory_hostname: 'name'
    expand_hostvars: true
    fail_on_errors: true
'''
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

import collections

from ansible.errors import AnsibleParserError
from ansible.plugins.inventory import BaseInventoryPlugin

try:
    import os_client_config
    import shade
    import shade.inventory
    HAS_SHADE = True
except ImportError:
    HAS_SHADE = False


class InventoryModule(BaseInventoryPlugin):
    ''' Host inventory provider for ansible using OpenStack clouds. '''

    NAME = 'openstack'

    def parse(self, inventory, loader, path, cache=True):

        super(InventoryModule, self).parse(inventory, loader, path)

        cache_key = self.get_cache_prefix(path)

        # file is config file
        try:
            self._config_data = self.loader.load_from_file(path)
        except Exception as e:
            raise AnsibleParserError(e)

        if not self._config_data:
            # empty. this is not my config file
            return False
        if 'plugin' in self._config_data and self._config_data['plugin'] != self.NAME:
            # plugin config file, but not for us
            return False
        elif 'plugin' not in self._config_data and 'clouds' not in self._config_data:
            # it's not a clouds.yaml file either
            return False

        if not HAS_SHADE:
            self.display.warning(
                'shade is required for the OpenStack inventory plugin.'
                ' OpenStack inventory sources will be skipped.')
            return False

        # The user has pointed us at a clouds.yaml file. Use defaults for
        # everything.
        if 'clouds' in self._config_data:
            self._config_data = {}

        source_data = None
        if cache and cache_key in inventory.cache:
            try:
                source_data = inventory.cache[cache_key]
            except KeyError:
                pass

        if not source_data:
            clouds_yaml_path = self._config_data.get('clouds_yaml_path')
            if clouds_yaml_path:
                config_files = (clouds_yaml_path +
                                os_client_config.config.CONFIG_FILES)
            else:
                config_files = None

            # TODO(mordred) Integrate shade's logging with ansible's logging
            shade.simple_logging()

            cloud_inventory = shade.inventory.OpenStackInventory(
                config_files=config_files,
                private=self._config_data.get('private', False))
            only_clouds = self._config_data.get('only_clouds', [])
            if only_clouds and not isinstance(only_clouds, list):
                raise ValueError(
                    'OpenStack Inventory Config Error: only_clouds must be'
                    ' a list')
            if only_clouds:
                new_clouds = []
                for cloud in cloud_inventory.clouds:
                    if cloud.name in only_clouds:
                        new_clouds.append(cloud)
                cloud_inventory.clouds = new_clouds

            expand_hostvars = self._config_data.get('expand_hostvars', False)
            fail_on_errors = self._config_data.get('fail_on_errors', False)

            source_data = cloud_inventory.list_hosts(
                expand=expand_hostvars, fail_on_cloud_config=fail_on_errors)

            inventory.cache[cache_key] = source_data

        self._populate_from_source(source_data)

    def _populate_from_source(self, source_data):
        groups = collections.defaultdict(list)
        firstpass = collections.defaultdict(list)
        hostvars = {}

        use_server_id = (
            self._config_data.get('inventory_hostname', 'name') != 'name')
        show_all = self._config_data.get('show_all', False)

        for server in source_data:
            if 'interface_ip' not in server and not show_all:
                continue
            firstpass[server['name']].append(server)

        for name, servers in firstpass.items():
            if len(servers) == 1 and not use_server_id:
                self._append_hostvars(hostvars, groups, name, servers[0])
            else:
                server_ids = set()
                # Trap for duplicate results
                for server in servers:
                    server_ids.add(server['id'])
                if len(server_ids) == 1 and not use_server_id:
                    self._append_hostvars(hostvars, groups, name, servers[0])
                else:
                    for server in servers:
                        self._append_hostvars(
                            hostvars, groups, server['id'], server,
                            namegroup=True)

        self._set_variables(hostvars, groups)

    def _set_variables(self, hostvars, groups):

        # set vars in inventory from hostvars
        for host in hostvars:

            # create composite vars
            self._set_composite_vars(
                self._config_data.get('compose'), hostvars, host)

            # actually update inventory
            for key in hostvars[host]:
                self.inventory.set_variable(host, key, hostvars[host][key])

            # constructed groups based on conditionals
            self._add_host_to_composed_groups(
                self._config_data.get('groups'), hostvars, host)

        for group_name, group_hosts in groups.items():
            self.inventory.add_group(group_name)
            for host in group_hosts:
                self.inventory.add_child(group_name, host)

    def _get_groups_from_server(self, server_vars, namegroup=True):
        groups = []

        region = server_vars['region']
        cloud = server_vars['cloud']
        metadata = server_vars.get('metadata', {})

        # Create a group for the cloud
        groups.append(cloud)

        # Create a group on region
        groups.append(region)

        # And one by cloud_region
        groups.append("%s_%s" % (cloud, region))

        # Check if group metadata key in servers' metadata
        if 'group' in metadata:
            groups.append(metadata['group'])

        for extra_group in metadata.get('groups', '').split(','):
            if extra_group:
                groups.append(extra_group.strip())

        groups.append('instance-%s' % server_vars['id'])
        if namegroup:
            groups.append(server_vars['name'])

        for key in ('flavor', 'image'):
            if 'name' in server_vars[key]:
                groups.append('%s-%s' % (key, server_vars[key]['name']))

        for key, value in iter(metadata.items()):
            groups.append('meta-%s_%s' % (key, value))

        az = server_vars.get('az', None)
        if az:
            # Make groups for az, region_az and cloud_region_az
            groups.append(az)
            groups.append('%s_%s' % (region, az))
            groups.append('%s_%s_%s' % (cloud, region, az))
        return groups

    def _append_hostvars(self, hostvars, groups, current_host,
                         server, namegroup=False):
        hostvars[current_host] = dict(
            ansible_ssh_host=server['interface_ip'],
            ansible_host=server['interface_ip'],
            openstack=server)
        self.inventory.add_host(current_host)

        for group in self._get_groups_from_server(server, namegroup=namegroup):
            groups[group].append(current_host)

    def verify_file(self, path):

        if super(InventoryModule, self).verify_file(path):
            for fn in ('openstack', 'clouds'):
                for suffix in ('yaml', 'yml'):
                    maybe = '{fn}.{suffix}'.format(fn=fn, suffix=suffix)
                    if path.endswith(maybe):
                        return True
        return False