2021-08-07 15:02:21 +02:00
|
|
|
# -*- coding: utf-8 -*-
|
2020-03-09 10:11:07 +01:00
|
|
|
# Copyright (c) 2017 Ansible Project
|
2022-08-05 12:28:29 +02:00
|
|
|
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
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:
|
|
|
|
- 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.
|
2020-12-27 13:53:41 +01:00
|
|
|
- The default inventory groups are built from groups (deprecated by
|
|
|
|
Linode) and not tags.
|
|
|
|
extends_documentation_fragment:
|
|
|
|
- constructed
|
2022-02-18 23:15:29 +01:00
|
|
|
- inventory_cache
|
2020-03-09 10:11:07 +01:00
|
|
|
options:
|
2022-02-18 23:15:29 +01:00
|
|
|
cache:
|
|
|
|
version_added: 4.5.0
|
|
|
|
cache_plugin:
|
|
|
|
version_added: 4.5.0
|
|
|
|
cache_timeout:
|
|
|
|
version_added: 4.5.0
|
|
|
|
cache_connection:
|
|
|
|
version_added: 4.5.0
|
|
|
|
cache_prefix:
|
|
|
|
version_added: 4.5.0
|
2020-03-09 10:11:07 +01:00
|
|
|
plugin:
|
2021-08-17 07:05:02 +02:00
|
|
|
description: Marks this as an instance of the 'linode' plugin.
|
2020-03-09 10:11:07 +01:00
|
|
|
required: true
|
2020-08-08 22:04:34 +02:00
|
|
|
choices: ['linode', 'community.general.linode']
|
2021-08-27 06:20:04 +02:00
|
|
|
ip_style:
|
|
|
|
description: Populate hostvars with all information available from the Linode APIv4.
|
|
|
|
type: string
|
2021-09-06 22:37:10 +02:00
|
|
|
default: plain
|
2021-08-27 06:20:04 +02:00
|
|
|
choices:
|
|
|
|
- plain
|
|
|
|
- api
|
|
|
|
version_added: 3.6.0
|
2020-03-09 10:11:07 +01:00
|
|
|
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
|
2022-03-14 20:56:27 +01:00
|
|
|
elements: string
|
2021-01-17 12:29:20 +01:00
|
|
|
tags:
|
|
|
|
description: Populate inventory only with instances which have at least one of the tags listed here.
|
|
|
|
default: []
|
|
|
|
type: list
|
2022-03-14 20:56:27 +01:00
|
|
|
elements: string
|
2021-01-17 12:29:20 +01:00
|
|
|
version_added: 2.0.0
|
2020-03-09 10:11:07 +01:00
|
|
|
types:
|
|
|
|
description: Populate inventory with instances with this type.
|
|
|
|
default: []
|
|
|
|
type: list
|
2022-03-14 20:56:27 +01:00
|
|
|
elements: string
|
2020-12-27 13:53:41 +01:00
|
|
|
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
|
2020-03-09 10:11:07 +01:00
|
|
|
'''
|
|
|
|
|
|
|
|
EXAMPLES = r'''
|
|
|
|
# Minimal example. `LINODE_ACCESS_TOKEN` is exposed in environment.
|
2020-08-08 22:04:34 +02:00
|
|
|
plugin: community.general.linode
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2022-01-30 22:16:59 +01:00
|
|
|
# You can use Jinja to template the access token.
|
|
|
|
plugin: community.general.linode
|
|
|
|
access_token: "{{ lookup('ini', 'token', section='your_username', file='~/.config/linode-cli') }}"
|
|
|
|
# For older Ansible versions, you need to write this as:
|
|
|
|
# access_token: "{{ lookup('ini', 'token section=your_username file=~/.config/linode-cli') }}"
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
# Example with regions, types, groups and access token
|
2020-08-08 22:04:34 +02:00
|
|
|
plugin: community.general.linode
|
2020-03-09 10:11:07 +01:00
|
|
|
access_token: foobar
|
|
|
|
regions:
|
|
|
|
- eu-west
|
|
|
|
types:
|
|
|
|
- g5-standard-2
|
2020-12-27 13:53:41 +01:00
|
|
|
|
|
|
|
# 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:
|
2021-08-15 13:11:16 +02:00
|
|
|
# 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]
|
2020-12-27 13:53:41 +01:00
|
|
|
ansible_port: 2222
|
2021-08-27 06:20:04 +02:00
|
|
|
|
|
|
|
# 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"
|
2020-03-09 10:11:07 +01:00
|
|
|
'''
|
|
|
|
|
2023-02-12 19:48:39 +01:00
|
|
|
from ansible.errors import AnsibleError
|
2022-02-18 23:15:29 +01:00
|
|
|
from ansible.plugins.inventory import BaseInventoryPlugin, Constructable, Cacheable
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
from linode_api4 import LinodeClient
|
2022-02-18 23:15:29 +01:00
|
|
|
from linode_api4.objects.linode import Instance
|
2020-03-09 10:11:07 +01:00
|
|
|
from linode_api4.errors import ApiError as LinodeApiError
|
2020-11-13 21:34:56 +01:00
|
|
|
HAS_LINODE = True
|
2020-03-09 10:11:07 +01:00
|
|
|
except ImportError:
|
2020-11-13 21:34:56 +01:00
|
|
|
HAS_LINODE = False
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
|
2022-02-18 23:15:29 +01:00
|
|
|
class InventoryModule(BaseInventoryPlugin, Constructable, Cacheable):
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
NAME = 'community.general.linode'
|
|
|
|
|
2022-01-30 22:16:59 +01:00
|
|
|
def _build_client(self, loader):
|
2020-03-09 10:11:07 +01:00
|
|
|
"""Build the Linode client."""
|
|
|
|
|
|
|
|
access_token = self.get_option('access_token')
|
2022-11-01 07:33:43 +01:00
|
|
|
if self.templar.is_template(access_token):
|
|
|
|
access_token = self.templar.template(variable=access_token, disable_lookups=False)
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
if access_token is None:
|
|
|
|
raise AnsibleError((
|
|
|
|
'Could not retrieve Linode access token '
|
2022-11-01 07:33:43 +01:00
|
|
|
'from plugin configuration sources'
|
2020-03-09 10:11:07 +01:00
|
|
|
))
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
2022-03-14 20:38:32 +01:00
|
|
|
def _filter_by_config(self):
|
2020-03-09 10:11:07 +01:00
|
|
|
"""Filter instances by user specified configuration."""
|
2022-03-14 20:38:32 +01:00
|
|
|
regions = self.get_option('regions')
|
2020-03-09 10:11:07 +01:00
|
|
|
if regions:
|
|
|
|
self.instances = [
|
|
|
|
instance for instance in self.instances
|
|
|
|
if instance.region.id in regions
|
|
|
|
]
|
|
|
|
|
2022-03-14 20:38:32 +01:00
|
|
|
types = self.get_option('types')
|
2020-03-09 10:11:07 +01:00
|
|
|
if types:
|
|
|
|
self.instances = [
|
|
|
|
instance for instance in self.instances
|
|
|
|
if instance.type.id in types
|
|
|
|
]
|
|
|
|
|
2022-03-14 20:38:32 +01:00
|
|
|
tags = self.get_option('tags')
|
2021-01-17 12:29:20 +01:00
|
|
|
if tags:
|
|
|
|
self.instances = [
|
|
|
|
instance for instance in self.instances
|
|
|
|
if any(tag in instance.tags for tag in tags)
|
|
|
|
]
|
|
|
|
|
2020-03-09 10:11:07 +01:00
|
|
|
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."""
|
2021-08-27 06:20:04 +02:00
|
|
|
ip_style = self.get_option('ip_style')
|
2020-03-09 10:11:07 +01:00
|
|
|
for instance in self.instances:
|
|
|
|
hostvars = instance._raw_json
|
|
|
|
for hostvar_key in hostvars:
|
2021-08-27 06:20:04 +02:00
|
|
|
if ip_style == 'api' and hostvar_key in ['ipv4', 'ipv6']:
|
|
|
|
continue
|
2020-03-09 10:11:07 +01:00
|
|
|
self.inventory.set_variable(
|
|
|
|
instance.label,
|
|
|
|
hostvar_key,
|
|
|
|
hostvars[hostvar_key]
|
|
|
|
)
|
2021-08-27 06:20:04 +02:00
|
|
|
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
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2022-02-18 23:15:29 +01:00
|
|
|
def _cacheable_inventory(self):
|
|
|
|
return [i._raw_json for i in self.instances]
|
2020-03-09 10:11:07 +01:00
|
|
|
|
2022-03-14 20:38:32 +01:00
|
|
|
def populate(self):
|
2020-12-27 13:53:41 +01:00
|
|
|
strict = self.get_option('strict')
|
2022-03-14 20:38:32 +01:00
|
|
|
|
|
|
|
self._filter_by_config()
|
2020-03-09 10:11:07 +01:00
|
|
|
|
|
|
|
self._add_groups()
|
|
|
|
self._add_instances_to_groups()
|
|
|
|
self._add_hostvars_for_instances()
|
2020-12-27 13:53:41 +01:00
|
|
|
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)
|
2022-02-18 23:15:29 +01:00
|
|
|
|
|
|
|
def verify_file(self, path):
|
2024-03-24 22:51:48 +01:00
|
|
|
"""Verify the Linode configuration file.
|
|
|
|
|
|
|
|
Return true/false if the config-file is valid for this plugin
|
|
|
|
|
|
|
|
Args:
|
|
|
|
str(path): path to the config
|
|
|
|
Kwargs:
|
|
|
|
None
|
|
|
|
Raises:
|
|
|
|
None
|
|
|
|
Returns:
|
|
|
|
bool(valid): is valid config file"""
|
|
|
|
valid = False
|
2022-02-18 23:15:29 +01:00
|
|
|
if super(InventoryModule, self).verify_file(path):
|
2024-03-24 22:51:48 +01:00
|
|
|
if path.endswith(("linode.yaml", "linode.yml")):
|
|
|
|
valid = True
|
|
|
|
else:
|
|
|
|
self.display.vvv('Inventory source not ending in "linode.yaml" or "linode.yml"')
|
|
|
|
return valid
|
2022-02-18 23:15:29 +01:00
|
|
|
|
|
|
|
def parse(self, inventory, loader, path, cache=True):
|
|
|
|
"""Dynamically parse Linode the cloud inventory."""
|
|
|
|
super(InventoryModule, self).parse(inventory, loader, path)
|
|
|
|
self.instances = None
|
|
|
|
|
|
|
|
if not HAS_LINODE:
|
|
|
|
raise AnsibleError('the Linode dynamic inventory plugin requires linode_api4.')
|
|
|
|
|
2022-03-14 20:38:32 +01:00
|
|
|
self._read_config_data(path)
|
2022-02-18 23:15:29 +01:00
|
|
|
|
|
|
|
cache_key = self.get_cache_key(path)
|
|
|
|
|
|
|
|
if cache:
|
|
|
|
cache = self.get_option('cache')
|
|
|
|
|
|
|
|
update_cache = False
|
|
|
|
if cache:
|
|
|
|
try:
|
|
|
|
self.instances = [Instance(None, i["id"], i) for i in self._cache[cache_key]]
|
|
|
|
except KeyError:
|
|
|
|
update_cache = True
|
|
|
|
|
|
|
|
# Check for None rather than False in order to allow
|
|
|
|
# for empty sets of cached instances
|
|
|
|
if self.instances is None:
|
|
|
|
self._build_client(loader)
|
|
|
|
self._get_instances_inventory()
|
|
|
|
|
|
|
|
if update_cache:
|
|
|
|
self._cache[cache_key] = self._cacheable_inventory()
|
|
|
|
|
2022-03-14 20:38:32 +01:00
|
|
|
self.populate()
|