summaryrefslogtreecommitdiff
path: root/lib/ansible/plugins/inventory/hcloud.py
blob: 7bb7503359dd7e30b2640b5170a1d28c5525eeea (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
# Copyright (c) 2019 Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import (absolute_import, division, print_function)

__metaclass__ = type

DOCUMENTATION = r"""
    name: hcloud
    plugin_type: inventory
    author:
      - Lukas Kaemmerling (@lkaemmerling)
    short_description: Ansible dynamic inventory plugin for the Hetzner Cloud.
    version_added: "2.8"
    requirements:
        - python >= 2.7
        - hcloud-python >= 1.0.0
    description:
        - Reads inventories from the Hetzner Cloud API.
        - Uses a YAML configuration file that ends with hcloud.(yml|yaml).
    extends_documentation_fragment:
        - constructed
    options:
        plugin:
            description: marks this as an instance of the "hcloud" plugin
            required: true
            choices: ["hcloud"]
        token:
            description: The Hetzner Cloud API Token.
            required: true
            env:
                - name: HCLOUD_TOKEN
        connect_with:
            description: Connect to the server using the value from this field.
            default: public_ipv4
            type: str
            choices:
                - public_ipv4
                - hostname
                - ipv4_dns_ptr
        locations:
          description: Populate inventory with instances in this location.
          default: []
          type: list
          required: false
        types:
          description: Populate inventory with instances with this type.
          default: []
          type: list
          required: false
        images:
          description: Populate inventory with instances with this image name, only available for system images.
          default: []
          type: list
          required: false
        label_selector:
          description: Populate inventory with instances with this label.
          default: ""
          type: str
          required: false
        network:
          description: Populate inventory with instances which are attached to this network name or ID.
          default: ""
          type: str
          required: false
"""

EXAMPLES = r"""
# Minimal example. `HCLOUD_TOKEN` is exposed in environment.
plugin: hcloud

# Example with locations, types, groups and token
plugin: hcloud
token: foobar
locations:
  - nbg1
types:
  - cx11

# Group by a location with prefix e.g. "hcloud_location_nbg1"
# and image_os_flavor without prefix and separator e.g. "ubuntu"
# and status with prefix e.g. "server_status_running"
plugin: hcloud
keyed_groups:
  - key: location
    prefix: hcloud_location
  - key: image_os_flavor
    separator: ""
  - key: status
    prefix: server_status
"""

import os
from ansible.errors import AnsibleError
from ansible.module_utils._text import to_native
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
from ansible.release import __version__

try:
    from hcloud import hcloud
except ImportError:
    raise AnsibleError("The Hetzner Cloud dynamic inventory plugin requires hcloud-python.")


class InventoryModule(BaseInventoryPlugin, Constructable):
    NAME = "hcloud"

    def _configure_hcloud_client(self):
        self.api_token = self.get_option("token")
        if self.api_token is None:
            raise AnsibleError(
                "Please specify a token, via the option token or via environment variable HCLOUD_TOKEN")

        self.endpoint = os.getenv("HCLOUD_ENDPOINT") or "https://api.hetzner.cloud/v1"

        self.client = hcloud.Client(token=self.api_token,
                                    api_endpoint=self.endpoint,
                                    application_name="ansible-inventory",
                                    application_version=__version__)

    def _test_hcloud_token(self):
        try:
            # We test the API Token against the location API, because this is the API with the smallest result
            # and not controllable from the customer.
            self.client.locations.get_all()
        except hcloud.APIException:
            raise AnsibleError("Invalid Hetzner Cloud API Token.")

    def _get_servers(self):
        if len(self.get_option("label_selector")) > 0:
            self.servers = self.client.servers.get_all(label_selector=self.get_option("label_selector"))
        else:
            self.servers = self.client.servers.get_all()

    def _filter_servers(self):
        if self.get_option("network"):
            try:
                self.network = self.client.networks.get_by_name(self.get_option("network"))
                if self.network is None:
                    self.network = self.client.networks.get_by_id(self.get_option("network"))
            except hcloud.APIException:
                raise AnsibleError(
                    "The given network is not found.")

            tmp = []
            for server in self.servers:
                for server_private_network in server.private_net:
                    if server_private_network.network.id == self.network.id:
                        tmp.append(server)
            self.servers = tmp

        if self.get_option("locations"):
            tmp = []
            for server in self.servers:
                if server.datacenter.location.name in self.get_option("locations"):
                    tmp.append(server)
            self.servers = tmp

        if self.get_option("types"):
            tmp = []
            for server in self.servers:
                if server.server_type.name in self.get_option("types"):
                    tmp.append(server)
            self.servers = tmp

        if self.get_option("images"):
            tmp = []
            for server in self.servers:
                if server.image is not None and server.image.os_flavor in self.get_option("images"):
                    tmp.append(server)
            self.servers = tmp

    def _set_server_attributes(self, server):
        self.inventory.set_variable(server.name, "id", to_native(server.id))
        self.inventory.set_variable(server.name, "name", to_native(server.name))
        self.inventory.set_variable(server.name, "status", to_native(server.status))
        self.inventory.set_variable(server.name, "type", to_native(server.server_type.name))

        # Network
        self.inventory.set_variable(server.name, "ipv4", to_native(server.public_net.ipv4.ip))
        self.inventory.set_variable(server.name, "ipv6_network", to_native(server.public_net.ipv6.network))
        self.inventory.set_variable(server.name, "ipv6_network_mask", to_native(server.public_net.ipv6.network_mask))

        if self.get_option("network"):
            for server_private_network in server.private_net:
                if server_private_network.network.id == self.network.id:
                    self.inventory.set_variable(server.name, "private_ipv4", to_native(server_private_network.ip))

        if self.get_option("connect_with") == "public_ipv4":
            self.inventory.set_variable(server.name, "ansible_host", to_native(server.public_net.ipv4.ip))
        elif self.get_option("connect_with") == "hostname":
            self.inventory.set_variable(server.name, "ansible_host", to_native(server.name))
        elif self.get_option("connect_with") == "ipv4_dns_ptr":
            self.inventory.set_variable(server.name, "ansible_host", to_native(server.public_net.ipv4.dns_ptr))
        elif self.get_option("connect_with") == "private_ipv4":
            if self.get_option("network"):
                for server_private_network in server.private_net:
                    if server_private_network.network.id == self.network.id:
                        self.inventory.set_variable(server.name, "ansible_host", to_native(server_private_network.ip))
            else:
                raise AnsibleError(
                    "You can only connect via private IPv4 if you specify a network")

        # Server Type
        if server.image is not None and server.image.name is not None:
            self.inventory.set_variable(server.name, "server_type", to_native(server.image.name))
        else:
            self.inventory.set_variable(server.name, "server_type", to_native("No Image name found."))

        # Datacenter
        self.inventory.set_variable(server.name, "datacenter", to_native(server.datacenter.name))
        self.inventory.set_variable(server.name, "location", to_native(server.datacenter.location.name))

        # Image
        if server.image is not None:
            self.inventory.set_variable(server.name, "image_id", to_native(server.image.id))
            self.inventory.set_variable(server.name, "image_os_flavor", to_native(server.image.os_flavor))
            if server.image.name is not None:
                self.inventory.set_variable(server.name, "image_name", to_native(server.image.name))
            else:
                self.inventory.set_variable(server.name, "image_name", to_native(server.image.description))
        else:
            self.inventory.set_variable(server.name, "image_id", to_native("No Image ID found"))
            self.inventory.set_variable(server.name, "image_name", to_native("No Image Name found"))
            self.inventory.set_variable(server.name, "image_os_flavor", to_native("No Image OS Flavor found"))

        # Labels
        self.inventory.set_variable(server.name, "labels", dict(server.labels))

    def verify_file(self, path):
        """Return the possibly of a file being consumable by this plugin."""
        return (
            super(InventoryModule, self).verify_file(path) and
            path.endswith((self.NAME + ".yaml", self.NAME + ".yml"))
        )

    def parse(self, inventory, loader, path, cache=True):
        super(InventoryModule, self).parse(inventory, loader, path, cache)
        self._read_config_data(path)
        self._configure_hcloud_client()
        self._test_hcloud_token()
        self._get_servers()
        self._filter_servers()

        # Add a top group 'hcloud'
        self.inventory.add_group(group="hcloud")

        for server in self.servers:
            self.inventory.add_host(server.name, group="hcloud")
            self._set_server_attributes(server)

            # Use constructed if applicable
            strict = self.get_option('strict')

            # Composed variables
            self._set_composite_vars(self.get_option('compose'), self.inventory.get_host(server.name).get_vars(), server.name, strict=strict)

            # Complex groups based on jinja2 conditionals, hosts that meet the conditional are added to group
            self._add_host_to_composed_groups(self.get_option('groups'), {}, server.name, strict=strict)

            # Create groups based on variable values and add the corresponding hosts to it
            self._add_host_to_keyed_groups(self.get_option('keyed_groups'), {}, server.name, strict=strict)