You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

407 lines
13 KiB

  1. #!/usr/bin/env python2
  2. #
  3. # Copyright 2015 Cisco Systems, Inc.
  4. #
  5. # Licensed under the Apache License, Version 2.0 (the "License");
  6. # you may not use this file except in compliance with the License.
  7. # You may obtain a copy of the License at
  8. #
  9. # http://www.apache.org/licenses/LICENSE-2.0
  10. #
  11. # Unless required by applicable law or agreed to in writing, software
  12. # distributed under the License is distributed on an "AS IS" BASIS,
  13. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. # See the License for the specific language governing permissions and
  15. # limitations under the License.
  16. #
  17. # original: https://github.com/CiscoCloud/terraform.py
  18. """\
  19. Dynamic inventory for Terraform - finds all `.tfstate` files below the working
  20. directory and generates an inventory based on them.
  21. """
  22. from __future__ import unicode_literals, print_function
  23. import argparse
  24. from collections import defaultdict
  25. from functools import wraps
  26. import json
  27. import os
  28. import re
  29. VERSION = '0.3.0pre'
  30. def tfstates(root=None):
  31. root = root or os.getcwd()
  32. for dirpath, _, filenames in os.walk(root):
  33. for name in filenames:
  34. if os.path.splitext(name)[-1] == '.tfstate':
  35. yield os.path.join(dirpath, name)
  36. def iterresources(filenames):
  37. for filename in filenames:
  38. with open(filename, 'r') as json_file:
  39. state = json.load(json_file)
  40. for module in state['modules']:
  41. name = module['path'][-1]
  42. for key, resource in module['resources'].items():
  43. yield name, key, resource
  44. ## READ RESOURCES
  45. PARSERS = {}
  46. def _clean_dc(dcname):
  47. # Consul DCs are strictly alphanumeric with underscores and hyphens -
  48. # ensure that the consul_dc attribute meets these requirements.
  49. return re.sub('[^\w_\-]', '-', dcname)
  50. def iterhosts(resources):
  51. '''yield host tuples of (name, attributes, groups)'''
  52. for module_name, key, resource in resources:
  53. resource_type, name = key.split('.', 1)
  54. try:
  55. parser = PARSERS[resource_type]
  56. except KeyError:
  57. continue
  58. yield parser(resource, module_name)
  59. def iterips(resources):
  60. '''yield ip tuples of (instance_id, ip)'''
  61. for module_name, key, resource in resources:
  62. resource_type, name = key.split('.', 1)
  63. if resource_type == 'openstack_compute_floatingip_associate_v2':
  64. yield openstack_floating_ips(resource)
  65. def parses(prefix):
  66. def inner(func):
  67. PARSERS[prefix] = func
  68. return func
  69. return inner
  70. def calculate_mantl_vars(func):
  71. """calculate Mantl vars"""
  72. @wraps(func)
  73. def inner(*args, **kwargs):
  74. name, attrs, groups = func(*args, **kwargs)
  75. # attrs
  76. if attrs.get('role', '') == 'control':
  77. attrs['consul_is_server'] = True
  78. else:
  79. attrs['consul_is_server'] = False
  80. # groups
  81. if attrs.get('publicly_routable', False):
  82. groups.append('publicly_routable')
  83. return name, attrs, groups
  84. return inner
  85. def _parse_prefix(source, prefix, sep='.'):
  86. for compkey, value in source.items():
  87. try:
  88. curprefix, rest = compkey.split(sep, 1)
  89. except ValueError:
  90. continue
  91. if curprefix != prefix or rest == '#':
  92. continue
  93. yield rest, value
  94. def parse_attr_list(source, prefix, sep='.'):
  95. attrs = defaultdict(dict)
  96. for compkey, value in _parse_prefix(source, prefix, sep):
  97. idx, key = compkey.split(sep, 1)
  98. attrs[idx][key] = value
  99. return attrs.values()
  100. def parse_dict(source, prefix, sep='.'):
  101. return dict(_parse_prefix(source, prefix, sep))
  102. def parse_list(source, prefix, sep='.'):
  103. return [value for _, value in _parse_prefix(source, prefix, sep)]
  104. def parse_bool(string_form):
  105. token = string_form.lower()[0]
  106. if token == 't':
  107. return True
  108. elif token == 'f':
  109. return False
  110. else:
  111. raise ValueError('could not convert %r to a bool' % string_form)
  112. @parses('packet_device')
  113. def packet_device(resource, tfvars=None):
  114. raw_attrs = resource['primary']['attributes']
  115. name = raw_attrs['hostname']
  116. groups = []
  117. attrs = {
  118. 'id': raw_attrs['id'],
  119. 'facilities': parse_list(raw_attrs, 'facilities'),
  120. 'hostname': raw_attrs['hostname'],
  121. 'operating_system': raw_attrs['operating_system'],
  122. 'locked': parse_bool(raw_attrs['locked']),
  123. 'tags': parse_list(raw_attrs, 'tags'),
  124. 'plan': raw_attrs['plan'],
  125. 'project_id': raw_attrs['project_id'],
  126. 'state': raw_attrs['state'],
  127. # ansible
  128. 'ansible_ssh_host': raw_attrs['network.0.address'],
  129. 'ansible_ssh_user': 'root', # it's always "root" on Packet
  130. # generic
  131. 'ipv4_address': raw_attrs['network.0.address'],
  132. 'public_ipv4': raw_attrs['network.0.address'],
  133. 'ipv6_address': raw_attrs['network.1.address'],
  134. 'public_ipv6': raw_attrs['network.1.address'],
  135. 'private_ipv4': raw_attrs['network.2.address'],
  136. 'provider': 'packet',
  137. }
  138. # add groups based on attrs
  139. groups.append('packet_operating_system=' + attrs['operating_system'])
  140. groups.append('packet_locked=%s' % attrs['locked'])
  141. groups.append('packet_state=' + attrs['state'])
  142. groups.append('packet_plan=' + attrs['plan'])
  143. # groups specific to kubespray
  144. groups = groups + attrs['tags']
  145. return name, attrs, groups
  146. def openstack_floating_ips(resource):
  147. raw_attrs = resource['primary']['attributes']
  148. attrs = {
  149. 'ip': raw_attrs['floating_ip'],
  150. 'instance_id': raw_attrs['instance_id'],
  151. }
  152. return attrs
  153. def openstack_floating_ips(resource):
  154. raw_attrs = resource['primary']['attributes']
  155. return raw_attrs['instance_id'], raw_attrs['floating_ip']
  156. @parses('openstack_compute_instance_v2')
  157. @calculate_mantl_vars
  158. def openstack_host(resource, module_name):
  159. raw_attrs = resource['primary']['attributes']
  160. name = raw_attrs['name']
  161. groups = []
  162. attrs = {
  163. 'access_ip_v4': raw_attrs['access_ip_v4'],
  164. 'access_ip_v6': raw_attrs['access_ip_v6'],
  165. 'access_ip': raw_attrs['access_ip_v4'],
  166. 'ip': raw_attrs['network.0.fixed_ip_v4'],
  167. 'flavor': parse_dict(raw_attrs, 'flavor',
  168. sep='_'),
  169. 'id': raw_attrs['id'],
  170. 'image': parse_dict(raw_attrs, 'image',
  171. sep='_'),
  172. 'key_pair': raw_attrs['key_pair'],
  173. 'metadata': parse_dict(raw_attrs, 'metadata'),
  174. 'network': parse_attr_list(raw_attrs, 'network'),
  175. 'region': raw_attrs.get('region', ''),
  176. 'security_groups': parse_list(raw_attrs, 'security_groups'),
  177. # ansible
  178. 'ansible_ssh_port': 22,
  179. # workaround for an OpenStack bug where hosts have a different domain
  180. # after they're restarted
  181. 'host_domain': 'novalocal',
  182. 'use_host_domain': True,
  183. # generic
  184. 'public_ipv4': raw_attrs['access_ip_v4'],
  185. 'private_ipv4': raw_attrs['access_ip_v4'],
  186. 'provider': 'openstack',
  187. }
  188. if 'floating_ip' in raw_attrs:
  189. attrs['private_ipv4'] = raw_attrs['network.0.fixed_ip_v4']
  190. try:
  191. if 'metadata.prefer_ipv6' in raw_attrs and raw_attrs['metadata.prefer_ipv6'] == "1":
  192. attrs.update({
  193. 'ansible_ssh_host': re.sub("[\[\]]", "", raw_attrs['access_ip_v6']),
  194. 'publicly_routable': True,
  195. })
  196. else:
  197. attrs.update({
  198. 'ansible_ssh_host': raw_attrs['access_ip_v4'],
  199. 'publicly_routable': True,
  200. })
  201. except (KeyError, ValueError):
  202. attrs.update({'ansible_ssh_host': '', 'publicly_routable': False})
  203. # Handling of floating IPs has changed: https://github.com/terraform-providers/terraform-provider-openstack/blob/master/CHANGELOG.md#010-june-21-2017
  204. # attrs specific to Ansible
  205. if 'metadata.ssh_user' in raw_attrs:
  206. attrs['ansible_ssh_user'] = raw_attrs['metadata.ssh_user']
  207. if 'volume.#' in raw_attrs.keys() and int(raw_attrs['volume.#']) > 0:
  208. device_index = 1
  209. for key, value in raw_attrs.items():
  210. match = re.search("^volume.*.device$", key)
  211. if match:
  212. attrs['disk_volume_device_'+str(device_index)] = value
  213. device_index += 1
  214. # attrs specific to Mantl
  215. attrs.update({
  216. 'consul_dc': _clean_dc(attrs['metadata'].get('dc', module_name)),
  217. 'role': attrs['metadata'].get('role', 'none'),
  218. 'ansible_python_interpreter': attrs['metadata'].get('python_bin','python')
  219. })
  220. # add groups based on attrs
  221. groups.append('os_image=' + attrs['image']['name'])
  222. groups.append('os_flavor=' + attrs['flavor']['name'])
  223. groups.extend('os_metadata_%s=%s' % item
  224. for item in attrs['metadata'].items())
  225. groups.append('os_region=' + attrs['region'])
  226. # groups specific to Mantl
  227. groups.append('role=' + attrs['metadata'].get('role', 'none'))
  228. groups.append('dc=' + attrs['consul_dc'])
  229. # groups specific to kubespray
  230. for group in attrs['metadata'].get('kubespray_groups', "").split(","):
  231. groups.append(group)
  232. return name, attrs, groups
  233. def iter_host_ips(hosts, ips):
  234. '''Update hosts that have an entry in the floating IP list'''
  235. for host in hosts:
  236. host_id = host[1]['id']
  237. if host_id in ips:
  238. ip = ips[host_id]
  239. host[1].update({
  240. 'access_ip_v4': ip,
  241. 'access_ip': ip,
  242. 'public_ipv4': ip,
  243. 'ansible_ssh_host': ip,
  244. })
  245. yield host
  246. ## QUERY TYPES
  247. def query_host(hosts, target):
  248. for name, attrs, _ in hosts:
  249. if name == target:
  250. return attrs
  251. return {}
  252. def query_list(hosts):
  253. groups = defaultdict(dict)
  254. meta = {}
  255. for name, attrs, hostgroups in hosts:
  256. for group in set(hostgroups):
  257. # Ansible 2.6.2 stopped supporting empty group names: https://github.com/ansible/ansible/pull/42584/commits/d4cd474b42ed23d8f8aabb2a7f84699673852eaf
  258. # Empty group name defaults to "all" in Ansible < 2.6.2 so we alter empty group names to "all"
  259. if not group: group = "all"
  260. groups[group].setdefault('hosts', [])
  261. groups[group]['hosts'].append(name)
  262. meta[name] = attrs
  263. groups['_meta'] = {'hostvars': meta}
  264. return groups
  265. def query_hostfile(hosts):
  266. out = ['## begin hosts generated by terraform.py ##']
  267. out.extend(
  268. '{}\t{}'.format(attrs['ansible_ssh_host'].ljust(16), name)
  269. for name, attrs, _ in hosts
  270. )
  271. out.append('## end hosts generated by terraform.py ##')
  272. return '\n'.join(out)
  273. def main():
  274. parser = argparse.ArgumentParser(
  275. __file__, __doc__,
  276. formatter_class=argparse.ArgumentDefaultsHelpFormatter, )
  277. modes = parser.add_mutually_exclusive_group(required=True)
  278. modes.add_argument('--list',
  279. action='store_true',
  280. help='list all variables')
  281. modes.add_argument('--host', help='list variables for a single host')
  282. modes.add_argument('--version',
  283. action='store_true',
  284. help='print version and exit')
  285. modes.add_argument('--hostfile',
  286. action='store_true',
  287. help='print hosts as a /etc/hosts snippet')
  288. parser.add_argument('--pretty',
  289. action='store_true',
  290. help='pretty-print output JSON')
  291. parser.add_argument('--nometa',
  292. action='store_true',
  293. help='with --list, exclude hostvars')
  294. default_root = os.environ.get('TERRAFORM_STATE_ROOT',
  295. os.path.abspath(os.path.join(os.path.dirname(__file__),
  296. '..', '..', )))
  297. parser.add_argument('--root',
  298. default=default_root,
  299. help='custom root to search for `.tfstate`s in')
  300. args = parser.parse_args()
  301. if args.version:
  302. print('%s %s' % (__file__, VERSION))
  303. parser.exit()
  304. hosts = iterhosts(iterresources(tfstates(args.root)))
  305. # Perform a second pass on the file to pick up floating_ip entries to update the ip address of referenced hosts
  306. ips = dict(iterips(iterresources(tfstates(args.root))))
  307. if ips:
  308. hosts = iter_host_ips(hosts, ips)
  309. if args.list:
  310. output = query_list(hosts)
  311. if args.nometa:
  312. del output['_meta']
  313. print(json.dumps(output, indent=4 if args.pretty else None))
  314. elif args.host:
  315. output = query_host(hosts, args.host)
  316. print(json.dumps(output, indent=4 if args.pretty else None))
  317. elif args.hostfile:
  318. output = query_hostfile(hosts)
  319. print(output)
  320. parser.exit()
  321. if __name__ == '__main__':
  322. main()