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