Repository: RedHatInsights/insights-core

Issue:
AWS EC2 metadata specs need to support IMDSv2
The current specs to collect metadata from AWS EC2 nodes only supports IMDSv1, and they need to be updated to be compatible with [IMDSv2 sessions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html).  The problem that this creates is the specs `aws_instance_id_doc` and `aws_instance_id_pkcs7` will execute on an EC2 node with IMDBv2 support only, and they will not generate an error, but no data is returned.  

The specs need to support a session token so that they can work with both IMDBv1 and v2.


Discussion:

Resolving patch:
diff --git a/insights/specs/datasources/aws.py b/insights/specs/datasources/aws.py
new file mode 100644
--- /dev/null
+++ b/insights/specs/datasources/aws.py
@@ -0,0 +1,42 @@
+"""
+Custom datasources for aws information
+"""
+from insights.components.cloud_provider import IsAWS
+from insights.core.context import HostContext
+from insights.core.dr import SkipComponent
+from insights.core.plugins import datasource
+from insights.core.spec_factory import simple_command
+from insights.specs import Specs
+
+
+class LocalSpecs(Specs):
+    """ Local specs used only by aws datasources """
+    aws_imdsv2_token = simple_command(
+        '/usr/bin/curl -s -X PUT "http://169.254.169.254/latest/api/token" -H "X-aws-ec2-metadata-token-ttl-seconds: 60" --connect-timeout 5',
+        deps=[IsAWS]
+    )
+
+
+@datasource(LocalSpecs.aws_imdsv2_token, HostContext)
+def aws_imdsv2_token(broker):
+    """
+    This datasource provides a session token for use by other specs to collect
+    metadata information on AWS EC2 nodes with IMDSv2 support..
+
+    Typical output of the input spec, which is also the output of this datasource::
+
+        AQAEABcCFaLcKRfXhLV9_ezugiVzra-qMBoPbdWGLrbdfqSLEJzP8w==
+
+    Returns:
+        str: String that is the actual session token to be used in other commands
+
+    Raises:
+        SkipComponent: When an error occurs or no token is generated
+    """
+    try:
+        token = broker[LocalSpecs.aws_imdsv2_token].content[0].strip()
+        if token:
+            return token
+    except Exception as e:
+        raise SkipComponent("Unexpected exception:{e}".format(e=str(e)))
+    raise SkipComponent
diff --git a/insights/specs/default.py b/insights/specs/default.py
--- a/insights/specs/default.py
+++ b/insights/specs/default.py
@@ -17,13 +17,13 @@
 from insights.core.spec_factory import first_of, command_with_args
 from insights.core.spec_factory import foreach_collect, foreach_execute
 from insights.core.spec_factory import first_file, listdir
-from insights.components.cloud_provider import IsAWS, IsAzure, IsGCP
+from insights.components.cloud_provider import IsAzure, IsGCP
 from insights.components.ceph import IsCephMonitor
 from insights.components.virtualization import IsBareMetal
 from insights.combiners.satellite_version import SatelliteVersion, CapsuleVersion
 from insights.specs import Specs
 from insights.specs.datasources import (
-    awx_manage, cloud_init, candlepin_broker, corosync as corosync_ds,
+    aws, awx_manage, cloud_init, candlepin_broker, corosync as corosync_ds,
     dir_list, ethernet, httpd, ipcs, kernel_module_list, lpstat, md5chk,
     package_provides, ps as ps_datasource, sap, satellite_missed_queues,
     ssl_certificate, system_user_dirs, user_group, yum_updates)
@@ -72,8 +72,8 @@ class DefaultSpecs(Specs):
     audit_log = simple_file("/var/log/audit/audit.log")
     avc_hash_stats = simple_file("/sys/fs/selinux/avc/hash_stats")
     avc_cache_threshold = simple_file("/sys/fs/selinux/avc/cache_threshold")
-    aws_instance_id_doc = simple_command("/usr/bin/curl -s http://169.254.169.254/latest/dynamic/instance-identity/document --connect-timeout 5", deps=[IsAWS])
-    aws_instance_id_pkcs7 = simple_command("/usr/bin/curl -s http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 --connect-timeout 5", deps=[IsAWS])
+    aws_instance_id_doc = command_with_args('/usr/bin/curl -s -H "X-aws-ec2-metadata-token: %s" http://169.254.169.254/latest/dynamic/instance-identity/document --connect-timeout 5', aws.aws_imdsv2_token, deps=[aws.aws_imdsv2_token])
+    aws_instance_id_pkcs7 = command_with_args('/usr/bin/curl -s -H "X-aws-ec2-metadata-token: %s" http://169.254.169.254/latest/dynamic/instance-identity/pkcs7 --connect-timeout 5', aws.aws_imdsv2_token, deps=[aws.aws_imdsv2_token])
     awx_manage_check_license = simple_command("/usr/bin/awx-manage check_license")
     awx_manage_check_license_data = awx_manage.awx_manage_check_license_data_datasource
     awx_manage_print_settings = simple_command("/usr/bin/awx-manage print_settings INSIGHTS_TRACKING_STATE SYSTEM_UUID INSTALL_UUID TOWER_URL_BASE AWX_CLEANUP_PATHS AWX_PROOT_BASE_PATH LOG_AGGREGATOR_ENABLED LOG_AGGREGATOR_LEVEL --format json")


Test patch:
diff --git a/insights/tests/datasources/test_aws.py b/insights/tests/datasources/test_aws.py
new file mode 100644
--- /dev/null
+++ b/insights/tests/datasources/test_aws.py
@@ -0,0 +1,30 @@
+import pytest
+from mock.mock import Mock
+from insights.core.dr import SkipComponent
+from insights.specs.datasources.aws import aws_imdsv2_token, LocalSpecs
+
+
+TOKEN = "1234567890\n"
+
+
+def test_aws_imdsv2_token():
+    input_spec = Mock()
+    input_spec.content = [TOKEN, ]
+    broker = {LocalSpecs.aws_imdsv2_token: input_spec}
+    results = aws_imdsv2_token(broker)
+    assert results == TOKEN.strip()
+
+
+def test_aws_imdsv2_token_exp():
+    input_spec = Mock()
+    input_spec.content = []
+    broker = {LocalSpecs.aws_imdsv2_token: input_spec}
+    with pytest.raises(Exception) as ex:
+        aws_imdsv2_token(broker)
+    assert "Unexpected" in str(ex)
+
+    input_spec = Mock()
+    input_spec.content = ["  ", ]
+    broker = {LocalSpecs.aws_imdsv2_token: input_spec}
+    with pytest.raises(SkipComponent) as ex:
+        aws_imdsv2_token(broker)