1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/plugins/inventory/linode.py
Kellin e77adff0b7
Linode Inventory can use full IP data from APIv4 (#3203)
* Linode Inventory can use full IP data from APIv4

- The Linode dynamic inventory module does not currently distinguish
  between private and public IP addresses even though the Linode APIv4
  contains this information. This change keeps the current behavior as
  the default and adds an option to set `ip_style: api`. When set, this
  option allows administrators to differentiate between private, public,
  slaac, local_link, and pool network addresses providing a more nuanced
  and granular view of the remote host's network information.

Signed-off-by: Kellin <kellin@retromud.org>

* Review - amend changelog details

- Adds a link back to this pull request
- Uses markdown styles for easier to read publishing in the changelogs
- Amends the wording style to match the existing changelog styles

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add scope to example invocation

- Adds the `community.general` scope to invocation example

Co-authored-by: Felix Fontein <felix@fontein.de>

* Convert lamda to list comprehension

- Change the ip type filter from a lambda to a list comprehension

Co-authored-by: Felix Fontein <felix@fontein.de>

* Add punctuation to description sentence

- Adds a period to the end of the description sentence

Co-authored-by: Felix Fontein <felix@fontein.de>

Co-authored-by: Felix Fontein <felix@fontein.de>
2021-08-27 06:20:04 +02:00

318 lines
10 KiB
Python

# -*- coding: utf-8 -*-
# Copyright (c) 2017 Ansible Project
# 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: linode
author:
- Luke Murphy (@decentral1se)
short_description: Ansible dynamic inventory plugin for Linode.
requirements:
- python >= 2.7
- linode_api4 >= 2.0.0
description:
- Reads inventories from the Linode API v4.
- Uses a YAML configuration file that ends with linode.(yml|yaml).
- Linode labels are used by default as the hostnames.
- The default inventory groups are built from groups (deprecated by
Linode) and not tags.
extends_documentation_fragment:
- constructed
options:
plugin:
description: Marks this as an instance of the 'linode' plugin.
required: true
choices: ['linode', 'community.general.linode']
ip_style:
description: Populate hostvars with all information available from the Linode APIv4.
type: string
default:
- plain
choices:
- plain
- api
version_added: 3.6.0
access_token:
description: The Linode account personal access token.
required: true
env:
- name: LINODE_ACCESS_TOKEN
regions:
description: Populate inventory with instances in this region.
default: []
type: list
tags:
description: Populate inventory only with instances which have at least one of the tags listed here.
default: []
type: list
version_added: 2.0.0
types:
description: Populate inventory with instances with this type.
default: []
type: list
strict:
version_added: 2.0.0
compose:
version_added: 2.0.0
groups:
version_added: 2.0.0
keyed_groups:
version_added: 2.0.0
'''
EXAMPLES = r'''
# Minimal example. `LINODE_ACCESS_TOKEN` is exposed in environment.
plugin: community.general.linode
# Example with regions, types, groups and access token
plugin: community.general.linode
access_token: foobar
regions:
- eu-west
types:
- g5-standard-2
# Example with keyed_groups, groups, and compose
plugin: community.general.linode
access_token: foobar
keyed_groups:
- key: tags
separator: ''
- key: region
prefix: region
groups:
webservers: "'web' in (tags|list)"
mailservers: "'mail' in (tags|list)"
compose:
# By default, Ansible tries to connect to the label of the instance.
# Since that might not be a valid name to connect to, you can
# replace it with the first IPv4 address of the linode as follows:
ansible_ssh_host: ipv4[0]
ansible_port: 2222
# Example where control traffic limited to internal network
plugin: community.general.linode
access_token: foobar
ip_style: api
compose:
ansible_host: "ipv4 | community.general.json_query('[?public==`false`].address') | first"
'''
import os
from ansible.errors import AnsibleError, AnsibleParserError
from ansible.module_utils.six import string_types
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable
try:
from linode_api4 import LinodeClient
from linode_api4.errors import ApiError as LinodeApiError
HAS_LINODE = True
except ImportError:
HAS_LINODE = False
class InventoryModule(BaseInventoryPlugin, Constructable):
NAME = 'community.general.linode'
def _build_client(self):
"""Build the Linode client."""
access_token = self.get_option('access_token')
if access_token is None:
try:
access_token = os.environ['LINODE_ACCESS_TOKEN']
except KeyError:
pass
if access_token is None:
raise AnsibleError((
'Could not retrieve Linode access token '
'from plugin configuration or environment'
))
self.client = LinodeClient(access_token)
def _get_instances_inventory(self):
"""Retrieve Linode instance information from cloud inventory."""
try:
self.instances = self.client.linode.instances()
except LinodeApiError as exception:
raise AnsibleError('Linode client raised: %s' % exception)
def _add_groups(self):
"""Add Linode instance groups to the dynamic inventory."""
self.linode_groups = set(
filter(None, [
instance.group
for instance
in self.instances
])
)
for linode_group in self.linode_groups:
self.inventory.add_group(linode_group)
def _filter_by_config(self, regions, types, tags):
"""Filter instances by user specified configuration."""
if regions:
self.instances = [
instance for instance in self.instances
if instance.region.id in regions
]
if types:
self.instances = [
instance for instance in self.instances
if instance.type.id in types
]
if tags:
self.instances = [
instance for instance in self.instances
if any(tag in instance.tags for tag in tags)
]
def _add_instances_to_groups(self):
"""Add instance names to their dynamic inventory groups."""
for instance in self.instances:
self.inventory.add_host(instance.label, group=instance.group)
def _add_hostvars_for_instances(self):
"""Add hostvars for instances in the dynamic inventory."""
ip_style = self.get_option('ip_style')
for instance in self.instances:
hostvars = instance._raw_json
for hostvar_key in hostvars:
if ip_style == 'api' and hostvar_key in ['ipv4', 'ipv6']:
continue
self.inventory.set_variable(
instance.label,
hostvar_key,
hostvars[hostvar_key]
)
if ip_style == 'api':
ips = instance.ips.ipv4.public + instance.ips.ipv4.private
ips += [instance.ips.ipv6.slaac, instance.ips.ipv6.link_local]
ips += instance.ips.ipv6.pools
for ip_type in set(ip.type for ip in ips):
self.inventory.set_variable(
instance.label,
ip_type,
self._ip_data([ip for ip in ips if ip.type == ip_type])
)
def _ip_data(self, ip_list):
data = []
for ip in list(ip_list):
data.append(
{
'address': ip.address,
'subnet_mask': ip.subnet_mask,
'gateway': ip.gateway,
'public': ip.public,
'prefix': ip.prefix,
'rdns': ip.rdns,
'type': ip.type
}
)
return data
def _validate_option(self, name, desired_type, option_value):
"""Validate user specified configuration data against types."""
if isinstance(option_value, string_types) and desired_type == list:
option_value = [option_value]
if option_value is None:
option_value = desired_type()
if not isinstance(option_value, desired_type):
raise AnsibleParserError(
'The option %s (%s) must be a %s' % (
name, option_value, desired_type
)
)
return option_value
def _get_query_options(self, config_data):
"""Get user specified query options from the configuration."""
options = {
'regions': {
'type_to_be': list,
'value': config_data.get('regions', [])
},
'types': {
'type_to_be': list,
'value': config_data.get('types', [])
},
'tags': {
'type_to_be': list,
'value': config_data.get('tags', [])
},
}
for name in options:
options[name]['value'] = self._validate_option(
name,
options[name]['type_to_be'],
options[name]['value']
)
regions = options['regions']['value']
types = options['types']['value']
tags = options['tags']['value']
return regions, types, tags
def verify_file(self, path):
"""Verify the Linode configuration file."""
if super(InventoryModule, self).verify_file(path):
endings = ('linode.yaml', 'linode.yml')
if any((path.endswith(ending) for ending in endings)):
return True
return False
def parse(self, inventory, loader, path, cache=True):
"""Dynamically parse Linode the cloud inventory."""
super(InventoryModule, self).parse(inventory, loader, path)
if not HAS_LINODE:
raise AnsibleError('the Linode dynamic inventory plugin requires linode_api4.')
config_data = self._read_config_data(path)
self._build_client()
self._get_instances_inventory()
strict = self.get_option('strict')
regions, types, tags = self._get_query_options(config_data)
self._filter_by_config(regions, types, tags)
self._add_groups()
self._add_instances_to_groups()
self._add_hostvars_for_instances()
for instance in self.instances:
variables = self.inventory.get_host(instance.label).get_vars()
self._add_host_to_composed_groups(
self.get_option('groups'),
variables,
instance.label,
strict=strict)
self._add_host_to_keyed_groups(
self.get_option('keyed_groups'),
variables,
instance.label,
strict=strict)
self._set_composite_vars(
self.get_option('compose'),
variables,
instance.label,
strict=strict)