Skip to content

Commit

Permalink
inventory aws_ec2 - add ssm_inventory inventory into hostvars (#1369)
Browse files Browse the repository at this point in the history
inventory aws_ec2 - add ssm_inventory inventory into hostvars

SUMMARY

using use_ssm_inventory users can populate hostvars with Amazon SSM inventory for configured instances
closes #704

ISSUE TYPE


Feature Pull Request

COMPONENT NAME

aws_ec2 inventory

Reviewed-by: Alina Buzachis
Reviewed-by: Mark Chappell
  • Loading branch information
abikouo authored Mar 7, 2023
1 parent 980f156 commit 5aa0e8a
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
minor_changes:
- inventory aws ec2 - add parameter `use_ssm_inventory` allowing to query ssm inventory information for configured EC2 instances and populate hostvars (https://github.com/ansible-collections/amazon.aws/issues/704).
5 changes: 5 additions & 0 deletions docs/docsite/rst/aws_ec2_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,11 @@ Now the output of ``ansible-inventory -i demo.aws_ec2.yml --list``:
``strict_permissions: False`` will ignore 403 errors rather than failing.


``use_ssm_inventory``
---------------------

``use_ssm_inventory: True`` will include SSM inventory variables into hostvars for ssm-configured instances.

``cache``
---------

Expand Down
33 changes: 30 additions & 3 deletions plugins/inventory/aws_ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@
- The suffix for host variables names coming from AWS.
type: str
version_added: 3.1.0
use_ssm_inventory:
description:
- Add SSM inventory information into hostvars.
type: bool
default: False
version_added: 6.0.0
"""

EXAMPLES = r"""
Expand Down Expand Up @@ -461,6 +467,11 @@ def _describe_ec2_instances(connection, filters):
return paginator.paginate(Filters=filters).build_full_result()


def _get_ssm_information(client, filters):
paginator = client.get_paginator("get_inventory")
return paginator.paginate(Filters=filters).build_full_result()


class InventoryModule(AWSInventoryBase):

NAME = 'amazon.aws.aws_ec2'
Expand Down Expand Up @@ -582,7 +593,7 @@ def _get_all_hostnames(self, instance, hostnames):

return hostname_list

def _query(self, regions, include_filters, exclude_filters, strict_permissions):
def _query(self, regions, include_filters, exclude_filters, strict_permissions, use_ssm_inventory):
'''
:param regions: a list of regions to query
:param include_filters: a list of boto3 filter dictionaries
Expand All @@ -609,8 +620,23 @@ def _query(self, regions, include_filters, exclude_filters, strict_permissions):

instances = sorted(instances, key=lambda x: x['InstanceId'])

if use_ssm_inventory and instances:
for connection, _region in self.all_clients("ssm"):
self._add_ssm_information(connection, instances)

return {'aws_ec2': instances}

def _add_ssm_information(self, connection, instances):
filters = [{"Key": "AWS:InstanceInformation.InstanceId", "Values": [x["InstanceId"] for x in instances]}]
result = _get_ssm_information(connection, filters)
for entity in result.get("Entities", []):
for x in instances:
if x["InstanceId"] == entity["Id"]:
content = entity.get("Data", {}).get("AWS:InstanceInformation", {}).get("Content", [])
if content:
x["SsmInventory"] = content[0]
break

def _populate(self, groups, hostnames, allow_duplicated_hosts=False,
hostvars_prefix=None, hostvars_suffix=None,
use_contrib_script_compatible_ec2_tag_keys=False):
Expand Down Expand Up @@ -701,7 +727,8 @@ def parse(self, inventory, loader, path, cache=True):

hostvars_prefix = self.get_option("hostvars_prefix")
hostvars_suffix = self.get_option("hostvars_suffix")
use_contrib_script_compatible_ec2_tag_keys = self.get_option('use_contrib_script_compatible_ec2_tag_keys')
use_contrib_script_compatible_ec2_tag_keys = self.get_option("use_contrib_script_compatible_ec2_tag_keys")
use_ssm_inventory = self.get_option("use_ssm_inventory")

if self.get_option('include_extra_api_calls'):
self.display.deprecate(
Expand All @@ -712,7 +739,7 @@ def parse(self, inventory, loader, path, cache=True):
result_was_cached, results = self.get_cached_result(path, cache)

if not result_was_cached:
results = self._query(regions, include_filters, exclude_filters, strict_permissions)
results = self._query(regions, include_filters, exclude_filters, strict_permissions, use_ssm_inventory)

self._populate(
results,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"Version": "2008-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": "ec2.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#!/usr/bin/python
# Copyright (c) 2023 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = """
module: test_get_ssm_inventory
short_description: Get SSM inventory information for EC2 instance
description:
- Gather SSM inventory for EC2 instance configured with SSM.
author: 'Aubin Bikouo (@abikouo)'
options:
instance_id:
description:
- EC2 instance id.
required: true
type: str
extends_documentation_fragment:
- amazon.aws.aws
- amazon.aws.ec2
- amazon.aws.boto3
"""


RETURN = """
ssm_inventory:
returned: on success
description: >
SSM inventory information.
type: dict
sample: {
'agent_type': 'amazon-ssm-agent',
'agent_version': '3.2.582.0',
'computer_name': 'ip-172-31-44-166.ec2.internal',
'instance_id': 'i-039eb9b1f55934ab6',
'instance_status': 'Active',
'ip_address': '172.31.44.166',
'platform_name': 'Fedora Linux',
'platform_type': 'Linux',
'platform_version': '37',
'resource_type': 'EC2Instance'
}
"""


from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import camel_dict_to_snake_dict


def main():
argument_spec = dict(instance_id=dict(required=True, type="str"))

module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True)

connection = module.client("ssm")

filters = [{"Key": "AWS:InstanceInformation.InstanceId", "Values": [module.params.get("instance_id")]}]
response = connection.get_inventory(Filters=filters)
entities = response.get("Entities", [])
ssm_inventory = {}
if entities:
content = entities[0].get("Data", {}).get("AWS:InstanceInformation", {}).get("Content", [])
if content:
ssm_inventory = camel_dict_to_snake_dict(content[0])
module.exit_json(ssm_inventory=ssm_inventory)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
---
- hosts: 127.0.0.1
connection: local
gather_facts: false
environment: "{{ ansible_test.environment }}"

collections:
- amazon.aws

module_defaults:
group/aws:
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token | default(omit) }}'
region: '{{ aws_region }}'

vars:
ami_details:
owner: 125523088429
name: Fedora-Cloud-Base-37-1.2.x86_64*
user_data: |
#!/bin/sh
sudo dnf install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm
sudo systemctl start amazon-ssm-agent
os_type: linux
iam_role_name: "{{ resource_prefix }}-inventory-ssm"

tasks:
- block:

# Create VPC, subnet, security group, and find image_id to create instance
- include_tasks: tasks/setup.yml

- name: Ensure IAM instance role exists
iam_role:
name: "{{ iam_role_name }}"
assume_role_policy_document: "{{ lookup('file', 'files/ec2-trust-policy.json') }}"
state: present
create_instance_profile: yes
managed_policy:
- AmazonSSMManagedInstanceCore
wait: True
register: role_output

- name: AMI Lookup (ami_info)
ec2_ami_info:
owners: '{{ ami_details.owner | default("amazon") }}'
filters:
name: '{{ ami_details.name }}'
register: ec2_amis
no_log: true

- name: Set facts with latest AMIs
vars:
latest_ami: '{{ ec2_amis.images | default([]) | sort(attribute="creation_date") | last }}'
set_fact:
latest_ami_id: '{{ ssm_amis | default(latest_ami.image_id) }}'

- name: Create EC2 instance
ec2_instance:
instance_type: "t3.micro"
ebs_optimized: True
image_id: "{{ latest_ami_id }}"
wait: "yes"
instance_role: "{{ role_output.iam_role.role_name }}"
name: "{{ resource_prefix }}-inventory-ssm"
user_data: "{{ ami_details.user_data }}"
state: running
tags:
TestPrefix: '{{ resource_prefix }}'
register: instance_output

- set_fact:
instances_ids: "{{ [instance_output.instance_ids[0]] }}"

- name: Get ssm inventory information
test_get_ssm_inventory:
instance_id: '{{ instance_output.instance_ids[0] }}'
aws_access_key: '{{ aws_access_key }}'
aws_secret_key: '{{ aws_secret_key }}'
security_token: '{{ security_token | default(omit) }}'
region: '{{ aws_region }}'
register: result
until: result.ssm_inventory != {}
retries: 18
delay: 10

- name: validate EC2 ssm-configured instance
assert:
that:
- result.ssm_inventory != {}

# Create 'Standard' EC2 instance (without ssm configured)
- name: Create another EC2 instance without SSM configured
amazon.aws.ec2_instance:
name: "{{ resource_prefix }}-inventory-std"
instance_type: "t3.micro"
image_id: "{{ latest_ami_id }}"
wait: true
state: running
register: _instance

- set_fact:
instances_ids: "{{ instances_ids + _instance.instance_ids }}"

# refresh inventory
- meta: refresh_inventory

- debug: var=hostvars

- name: assert hostvars was populated with ssm_inventory information
assert:
that:
- ssm_hostname in hostvars
- std_hostname in hostvars
- '"ssm_inventory" in hostvars[ssm_hostname]'
- hostvars[ssm_hostname].ssm_inventory["agent_type"] == "amazon-ssm-agent"
- hostvars[ssm_hostname].ssm_inventory["platform_type"] == "Linux"
- hostvars[ssm_hostname].ssm_inventory["platform_name"] == "Fedora Linux"
- '"ssm_inventory" not in hostvars[std_hostname]'
vars:
ssm_hostname: '{{ resource_prefix }}-inventory-ssm'
std_hostname: '{{ resource_prefix }}-inventory-std'

always:
- name: Delete IAM role
iam_role:
name: "{{ iam_role_name }}"
state: absent
wait: True

- name: Delete EC2 instances
amazon.aws.ec2_instance:
instance_ids: "{{ instances_ids }}"
wait: true
state: absent
when: instances_ids is defined
4 changes: 4 additions & 0 deletions tests/integration/targets/inventory_aws_ec2/runme.sh
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ ansible-playbook playbooks/create_inventory_config.yml -e "template='inventory_w
ansible-playbook playbooks/populate_cache.yml "$@"
ansible-playbook playbooks/test_inventory_cache.yml "$@"

# generate inventory config with ssm inventory information
ansible-playbook playbooks/create_inventory_config.yml -e "template='inventory_with_ssm.yml.j2'" "$@"
ansible-playbook playbooks/test_inventory_ssm.yml "$@"

# remove inventory cache
rm -r aws_ec2_cache_dir/

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
plugin: amazon.aws.aws_ec2
aws_access_key_id: '{{ aws_access_key }}'
aws_secret_access_key: '{{ aws_secret_key }}'
{% if security_token | default(false) %}
aws_security_token: '{{ security_token }}'
{% endif %}
regions:
- '{{ aws_region }}'
filters:
tag:Name:
- '{{ resource_prefix }}-inventory-*'
hostnames:
- tag:Name
use_ssm_inventory: true
Loading

0 comments on commit 5aa0e8a

Please sign in to comment.