Skip to content

Commit c184e2c

Browse files
committed
feat(plugins): add elasticache inventory
1 parent 1ad3f31 commit c184e2c

File tree

2 files changed

+705
-0
lines changed

2 files changed

+705
-0
lines changed

plugins/inventory/aws_ec.py

+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# -*- coding: utf-8 -*-
2+
3+
# Copyright (c) 2018 Ansible Project
4+
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5+
6+
DOCUMENTATION = r"""
7+
name: aws_ec
8+
short_description: Elasticache/ec inventory source
9+
description:
10+
- Get Cache from Amazon Web Services Elasticache.
11+
- Uses a YAML configuration file that ends with aws_ec.(yml|yaml).
12+
options:
13+
regions:
14+
description:
15+
- A list of regions in which to describe Elasticache instances and clusters. Available regions are listed here
16+
U(https://docs.aws.amazon.com/fr_fr/AmazonElastiCache/latest/red-ug/RegionsAndAZs.html).
17+
default: []
18+
filters:
19+
description:
20+
- A dictionary of filter value pairs. Available filters are listed here
21+
U(https://docs.aws.amazon.com/AmazonElastiCache/latest/APIReference/API_Filter.html).
22+
default: {}
23+
strict_permissions:
24+
description:
25+
- By default if an AccessDenied exception is encountered this plugin will fail. You can set strict_permissions to
26+
False in the inventory config file which will allow the restrictions to be gracefully skipped.
27+
type: bool
28+
default: True
29+
statuses:
30+
description: A list of desired states for instances/clusters to be added to inventory. Set to ['all'] as a shorthand to find everything.
31+
type: list
32+
elements: str
33+
default:
34+
- creating
35+
- available
36+
hostvars_prefix:
37+
description:
38+
- The prefix for host variables names coming from AWS.
39+
type: str
40+
version_added: 3.1.0
41+
hostvars_suffix:
42+
description:
43+
- The suffix for host variables names coming from AWS.
44+
type: str
45+
version_added: 3.1.0
46+
notes:
47+
- Ansible versions prior to 2.10 should use the fully qualified plugin name 'amazon.aws.aws_ec'.
48+
extends_documentation_fragment:
49+
- inventory_cache
50+
- constructed
51+
- amazon.aws.boto3
52+
- amazon.aws.common.plugins
53+
- amazon.aws.region.plugins
54+
- amazon.aws.assume_role.plugins
55+
author:
56+
- Your friendly neighbourhood Rafael (@Rafjt/@Raf211)
57+
"""
58+
59+
EXAMPLES = r"""
60+
plugin: amazon.aws.aws_ec
61+
regions:
62+
- us-east-1
63+
- ca-central-1
64+
hostvars_prefix: aws_
65+
hostvars_suffix: _ec
66+
"""
67+
68+
try:
69+
import botocore
70+
except ImportError:
71+
pass # will be captured by imported HAS_BOTO3
72+
73+
from ansible.errors import AnsibleError
74+
from ansible.module_utils._text import to_native
75+
from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
76+
from pprint import pprint
77+
78+
from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code
79+
from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict
80+
from ansible_collections.amazon.aws.plugins.module_utils.transformation import ansible_dict_to_boto3_filter_list
81+
from ansible_collections.amazon.aws.plugins.plugin_utils.inventory import AWSInventoryBase
82+
83+
def _find_ec_clusters_with_valid_statuses(replication_groups, cache_clusters, statuses):
84+
if "all" in statuses:
85+
return(replication_groups,cache_clusters)
86+
valid_clusters = []
87+
for replication_group in replication_groups:
88+
if replication_group.get("Status") in statuses:
89+
valid_clusters.append(replication_group)
90+
for cache_cluster in cache_clusters:
91+
if cache_cluster.get("CacheClusterStatus") in statuses:
92+
valid_clusters.append(cache_cluster)
93+
return valid_clusters
94+
95+
def _add_tags_for_ec_clusters(connection, clusters, strict):
96+
for cluster in clusters:
97+
if "ReplicationGroupId" in cluster:
98+
resource_arn = cluster["ARN"]
99+
try:
100+
tags = connection.list_tags_for_resource(ResourceName=resource_arn)["TagList"]
101+
except is_boto3_error_code("AccessDenied") as e:
102+
if not strict:
103+
tags = []
104+
else:
105+
raise e
106+
cluster["Tags"] = tags
107+
108+
def describe_resource_with_tags(func):
109+
def describe_wrapper(connection, strict=False):
110+
try:
111+
results = func(connection=connection,)
112+
if "ReplicationGroups" in results:
113+
results = results["ReplicationGroups"]
114+
else:
115+
results = results["CacheClusters"]
116+
_add_tags_for_ec_clusters(connection, results, strict)
117+
except is_boto3_error_code("AccessDenied") as e: # pylint: disable=duplicate-except
118+
if not strict:
119+
return []
120+
raise AnsibleError(f"Failed to query ElastiCache: {to_native(e)}")
121+
except (
122+
botocore.exceptions.BotoCoreError,
123+
botocore.exceptions.ClientError,
124+
) as e: # pylint: disable=duplicate-except
125+
raise AnsibleError(f"Failed to query ElastiCache: {to_native(e)}")
126+
127+
return results
128+
129+
return describe_wrapper
130+
131+
132+
@describe_resource_with_tags
133+
def _describe_replication_groups(connection):
134+
paginator = connection.get_paginator("describe_replication_groups")
135+
return paginator.paginate().build_full_result()
136+
137+
@describe_resource_with_tags
138+
def _describe_cache_clusters(connection):
139+
paginator = connection.get_paginator("describe_cache_clusters")
140+
return paginator.paginate().build_full_result()
141+
142+
143+
class InventoryModule(AWSInventoryBase):
144+
NAME = "amazon.aws.aws_ec"
145+
INVENTORY_FILE_SUFFIXES = ("aws_ec.yml", "aws_ec.yaml")
146+
147+
def __init__(self):
148+
super().__init__()
149+
self.credentials = {}
150+
151+
def _populate(self, replication_groups, cache_clusters):
152+
group = "aws_ec"
153+
cluster_group_name = "cluster_group"
154+
replication_group_name = "replication_group"
155+
156+
if replication_groups:
157+
self.inventory.add_group(replication_group_name)
158+
self._add_hosts(hosts=replication_groups, group=replication_group_name)
159+
self.inventory.add_child("all", replication_group_name)
160+
161+
if cache_clusters:
162+
self.inventory.add_group(cluster_group_name)
163+
self._add_hosts(hosts=cache_clusters, group=cluster_group_name)
164+
self.inventory.add_child("all", cluster_group_name)
165+
166+
def _populate_from_source(self, source_data):
167+
hostvars = source_data.pop("_meta", {}).get("hostvars", {})
168+
for group in source_data:
169+
if group == "all":
170+
continue
171+
self.inventory.add_group(group)
172+
hosts = source_data[group].get("hosts", [])
173+
for host in hosts:
174+
self._populate_host_vars([host], hostvars.get(host, {}), group)
175+
self.inventory.add_child("all", group)
176+
177+
178+
def _add_hosts(self, hosts, group):
179+
"""
180+
:param hosts: a list of hosts to be added to a group
181+
:param group: the name of the group to which the hosts belong
182+
"""
183+
for host in hosts:
184+
if "replicationgroup" == host["ARN"].split(":")[5]:
185+
host_type = "replicationgroup"
186+
host_name = host["ReplicationGroupId"]
187+
else:
188+
host_type = "cluster"
189+
host_name = host["CacheClusterId"]
190+
191+
host = camel_dict_to_snake_dict(host, ignore_list=["Tags"])
192+
host["tags"] = boto3_tag_list_to_ansible_dict(host.get("tags", []))
193+
host["type"] = host_type
194+
195+
if "availability_zone" in host:
196+
host["region"] = host["availability_zone"][:-1]
197+
elif "availability_zones" in host:
198+
host["region"] = host["availability_zones"][0][:-1]
199+
200+
self.inventory.add_host(host_name, group=group)
201+
hostvars_prefix = self.get_option("hostvars_prefix")
202+
hostvars_suffix = self.get_option("hostvars_suffix")
203+
new_vars = dict()
204+
for hostvar, hostval in host.items():
205+
if hostvars_prefix:
206+
hostvar = hostvars_prefix + hostvar
207+
if hostvars_suffix:
208+
hostvar = hostvar + hostvars_suffix
209+
new_vars[hostvar] = hostval
210+
self.inventory.set_variable(host_name, hostvar, hostval)
211+
host.update(new_vars)
212+
213+
strict = self.get_option("strict")
214+
self._set_composite_vars(self.get_option("compose"), host, host_name, strict=strict)
215+
self._add_host_to_composed_groups(self.get_option("groups"), host, host_name, strict=strict)
216+
self._add_host_to_keyed_groups(self.get_option("keyed_groups"), host, host_name, strict=strict)
217+
218+
219+
220+
def _get_all_replication_groups(self, regions, strict, statuses):
221+
replication_groups = []
222+
for connection, _region in self.all_clients("elasticache"):
223+
replication_groups += _describe_replication_groups(connection, strict=strict)
224+
sorted_replication_groups = sorted(replication_groups, key=lambda x: x["ReplicationGroupId"])
225+
return _find_ec_clusters_with_valid_statuses(sorted_replication_groups, [], statuses)
226+
227+
def _get_all_cache_clusters(self, regions, strict, statuses):
228+
cache_clusters = []
229+
for connection, _region in self.all_clients("elasticache"):
230+
cache_clusters += _describe_cache_clusters(connection, strict=strict)
231+
sorted_cache_clusters = sorted(cache_clusters, key=lambda x: x["CacheClusterId"])
232+
return _find_ec_clusters_with_valid_statuses([], sorted_cache_clusters, statuses)
233+
234+
235+
def parse(self, inventory, loader, path, cache=True):
236+
super().parse(inventory, loader, path, cache=cache)
237+
238+
regions = self.get_option("regions")
239+
strict_permissions = self.get_option("strict_permissions")
240+
statuses = self.get_option("statuses")
241+
242+
result_was_cached, cached_result = self.get_cached_result(path, cache)
243+
if result_was_cached:
244+
self._populate_from_source(cached_result)
245+
return
246+
247+
replication_groups = self._get_all_replication_groups(
248+
regions=regions,
249+
strict=strict_permissions,
250+
statuses=statuses,
251+
)
252+
253+
cache_clusters = self._get_all_cache_clusters(
254+
regions=regions,
255+
strict=strict_permissions,
256+
statuses=statuses,
257+
)
258+
259+
self._populate(replication_groups, cache_clusters)
260+

0 commit comments

Comments
 (0)