From ad89e49a46b807c4939d1ae17384930747dfd6d6 Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Tue, 7 Mar 2023 18:25:39 +0100 Subject: [PATCH] ssm_inventory_info - new module to retrieve ssm inventory for configured ec2 instances (#1745) ssm_inventory_info module SUMMARY new module to retrieve ssm inventory info for EC2 configured instances ISSUE TYPE New Module Pull Request COMPONENT NAME ssm_inventory_info Reviewed-by: Mark Chappell --- meta/runtime.yml | 1 + plugins/modules/ssm_inventory_info.py | 120 +++++++++++++++ .../modules/test_ssm_inventory_info.py | 145 ++++++++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 plugins/modules/ssm_inventory_info.py create mode 100644 tests/unit/plugins/modules/test_ssm_inventory_info.py diff --git a/meta/runtime.yml b/meta/runtime.yml index 6fd39ee0fc9..43343723648 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -200,6 +200,7 @@ action_groups: - sns_topic - sns_topic_info - sqs_queue + - ssm_inventory_info - ssm_parameter - stepfunctions_state_machine - stepfunctions_state_machine_execution diff --git a/plugins/modules/ssm_inventory_info.py b/plugins/modules/ssm_inventory_info.py new file mode 100644 index 00000000000..4242596f128 --- /dev/null +++ b/plugins/modules/ssm_inventory_info.py @@ -0,0 +1,120 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = """ +module: ssm_inventory_info +version_added: 6.0.0 +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.common.modules +- amazon.aws.region.modules +- amazon.aws.boto3 +""" + +EXAMPLES = """ +- name: Retrieve SSM inventory info for instance id 'i-012345678902' + community.aws.ssm_inventory_info: + instance_id: 'i-012345678902' +""" + + +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' + } +""" + + +try: + import botocore +except ImportError: + pass # Handled by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.community.aws.plugins.module_utils.modules import AnsibleCommunityAWSModule as AnsibleAWSModule + + +class SsmInventoryInfoFailure(Exception): + def __init__(self, exc, msg): + self.exc = exc + self.msg = msg + super().__init__(self) + + +def get_ssm_inventory(connection, filters): + try: + return connection.get_inventory(Filters=filters) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + raise SsmInventoryInfoFailure(exc=e, msg="get_ssm_inventory() failed.") + + +def execute_module(module, connection): + + instance_id = module.params.get("instance_id") + try: + filters = [ + { + "Key": "AWS:InstanceInformation.InstanceId", + "Values": [instance_id] + } + ] + + response = get_ssm_inventory(connection, 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(changed=False, ssm_inventory=ssm_inventory) + except SsmInventoryInfoFailure as e: + module.fail_json_aws(exception=e.exc, msg=e.msg) + + +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") + execute_module(module, connection) + + +if __name__ == "__main__": + main() diff --git a/tests/unit/plugins/modules/test_ssm_inventory_info.py b/tests/unit/plugins/modules/test_ssm_inventory_info.py new file mode 100644 index 00000000000..6c8559ae77b --- /dev/null +++ b/tests/unit/plugins/modules/test_ssm_inventory_info.py @@ -0,0 +1,145 @@ +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +import pytest +from botocore.exceptions import BotoCoreError + +from unittest.mock import MagicMock, patch +from ansible_collections.community.aws.plugins.modules.ssm_inventory_info import ( + execute_module, + get_ssm_inventory, + SsmInventoryInfoFailure, +) + + +def test_get_ssm_inventory(): + connection = MagicMock() + inventory_response = MagicMock() + connection.get_inventory.return_value = inventory_response + filters = MagicMock() + + assert get_ssm_inventory(connection, filters) == inventory_response + connection.get_inventory.assert_called_once_with( + Filters=filters + ) + + +def test_get_ssm_inventory_failure(): + connection = MagicMock() + connection.get_inventory.side_effect = BotoCoreError(error="failed", operation="get_ssm_inventory") + filters = MagicMock() + + with pytest.raises(SsmInventoryInfoFailure): + get_ssm_inventory(connection, filters) + + +@patch('ansible_collections.community.aws.plugins.modules.ssm_inventory_info.get_ssm_inventory') +def test_execute_module(m_get_ssm_inventory): + + instance_id = "i-0202020202020202" + aws_inventory = { + 'AgentType': 'amazon-ssm-agent', + 'AgentVersion': '3.2.582.0', + 'ComputerName': 'ip-172-31-44-166.ec2.internal', + 'InstanceId': 'i-039eb9b1f55934ab6', + 'InstanceStatus': 'Active', + 'IpAddress': '172.31.44.166', + 'PlatformName': 'Fedora Linux', + 'PlatformType': 'Linux', + 'PlatformVersion': '37', + 'ResourceType': 'EC2Instance' + } + + ansible_inventory = { + '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' + } + + m_get_ssm_inventory.return_value = { + "Entities": [ + { + 'Id': instance_id, + "Data": { + "AWS:InstanceInformation": {"Content": [aws_inventory]} + } + } + ], + "Status": 200 + } + + connection = MagicMock() + module = MagicMock() + module.params = dict( + instance_id=instance_id + ) + module.exit_json.side_effect = SystemExit(1) + module.fail_json_aws.side_effect = SystemError(2) + + with pytest.raises(SystemExit): + execute_module(module, connection) + + module.exit_json.assert_called_once_with( + changed=False, ssm_inventory=ansible_inventory + ) + + +@patch('ansible_collections.community.aws.plugins.modules.ssm_inventory_info.get_ssm_inventory') +def test_execute_module_no_data(m_get_ssm_inventory): + + instance_id = "i-0202020202020202" + + m_get_ssm_inventory.return_value = { + "Entities": [ + { + 'Id': instance_id, + "Data": {} + } + ], + } + + connection = MagicMock() + module = MagicMock() + module.params = dict( + instance_id=instance_id + ) + module.exit_json.side_effect = SystemExit(1) + module.fail_json_aws.side_effect = SystemError(2) + + with pytest.raises(SystemExit): + execute_module(module, connection) + + module.exit_json.assert_called_once_with( + changed=False, ssm_inventory={} + ) + + +@patch('ansible_collections.community.aws.plugins.modules.ssm_inventory_info.get_ssm_inventory') +def test_execute_module_failure(m_get_ssm_inventory): + + instance_id = "i-0202020202020202" + + m_get_ssm_inventory.side_effect = SsmInventoryInfoFailure( + exc=BotoCoreError(error="failed", operation="get_ssm_inventory"), + msg="get_ssm_inventory() failed." + ) + + connection = MagicMock() + module = MagicMock() + module.params = dict( + instance_id=instance_id + ) + module.exit_json.side_effect = SystemExit(1) + module.fail_json_aws.side_effect = SystemError(2) + + with pytest.raises(SystemError): + execute_module(module, connection)