Repository: cloud-custodian/cloud-custodian

Issue:
TypeError: 'unicode' object is not callable on c7n-mailer==0.5.2
after redeploy my mailer with update-lambda with c7n-mailer==0.5.2

we have an error when send an slack message with mailer 

Traceback (most recent call last):
  File "/var/task/c7n_mailer/sqs_queue_processor.py", line 185, in process_sqs_message
    slack_delivery.slack_handler(sqs_message, slack_messages)
  File "/var/task/c7n_mailer/slack_delivery.py", line 116, in slack_handler
    self.send_slack_msg(key, payload)
  File "/var/task/c7n_mailer/slack_delivery.py", line 189, in send_slack_msg
    response.status_code, response.text())
TypeError: 'unicode' object is not callable

after isolate a function send_slack_msg
line 189 :
response.status_code, response.text()

error fix : response.status_code, response.text

thanks for your support



Discussion:

Resolving patch:
diff --git a/tools/c7n_mailer/c7n_mailer/slack_delivery.py b/tools/c7n_mailer/c7n_mailer/slack_delivery.py
--- a/tools/c7n_mailer/c7n_mailer/slack_delivery.py
+++ b/tools/c7n_mailer/c7n_mailer/slack_delivery.py
@@ -90,10 +90,16 @@ def get_to_addrs_slack_messages_map(self, sqs_message):
                     resource_list,
                     self.logger, 'slack_template', 'slack_default',
                     self.config['templates_folders'])
-            elif target.startswith('slack://tag/') and 'tags' in resource:
+            elif target.startswith('slack://tag/') and 'Tags' in resource:
                 tag_name = target.split('tag/', 1)[1]
-                result = resource.get('tags', {}).get(tag_name, None)
-                resolved_addrs = result
+                result = next((item for item in resource.get('Tags', [])
+                               if item["Key"] == tag_name), None)
+                if not result:
+                    self.logger.debug(
+                        "No %s tag found in resource." % tag_name)
+                    continue
+
+                resolved_addrs = result['Value']
                 slack_messages[resolved_addrs] = get_rendered_jinja(
                     resolved_addrs, sqs_message,
                     resource_list,
@@ -143,7 +149,7 @@ def retrieve_user_im(self, email_addresses):
                 elif response["error"] == "invalid_auth":
                     raise Exception("Invalid Slack token.")
                 elif response["error"] == "users_not_found":
-                    self.logger.info("Slack user ID not found.")
+                    self.logger.info("Slack user ID for email address %s not found.", address)
                     if self.caching:
                         self.caching.set(address, {})
                     continue
@@ -177,14 +183,21 @@ def send_slack_msg(self, key, message_payload):
                 headers={'Content-Type': 'application/json;charset=utf-8',
                          'Authorization': 'Bearer %s' % self.config.get('slack_token')})
 
+        response_json = response.json()
         if response.status_code == 429 and "Retry-After" in response.headers:
             self.logger.info(
                 "Slack API rate limiting. Waiting %d seconds",
-                int(response.headers['retry-after']))
+                int(response.headers['Retry-After']))
             time.sleep(int(response.headers['Retry-After']))
             return
-        elif response.status_code != 200:  # pragma: no cover
+
+        elif response.status_code != 200:
             self.logger.info(
                 "Error in sending Slack message status:%s response: %s",
-                response.status_code, response.text())
+                response.status_code, response.text)
+            return
+
+        elif not response_json['ok']:
+            self.logger.info("Error in sending Slack message. Status:%s, response:%s",
+                             response.status_code, response_json['error'])
             return


Test patch:
diff --git a/tools/c7n_mailer/tests/common.py b/tools/c7n_mailer/tests/common.py
--- a/tools/c7n_mailer/tests/common.py
+++ b/tools/c7n_mailer/tests/common.py
@@ -97,6 +97,34 @@
     'Size': 8
 }
 
+RESOURCE_3 = {
+    'AvailabilityZone': 'us-east-1c',
+    "CreateTime": "2019-05-07T19:09:46.148Z",
+    'Attachments': [
+        {
+            "AttachTime": "2019-05-07T19:09:46.000Z",
+            "Device": "/dev/xvda",
+            "InstanceId": "i-00000000000000000",
+            "State": "attached",
+            "VolumeId": "vol-00000000000000000",
+            "DeleteOnTermination": 'true'
+        }
+    ],
+    'Tags': [
+        {
+            'Value': 'milton@initech.com',
+            'Key': 'SupportEmail'
+        },
+        {
+            'Value': 'peter',
+            'Key': 'CreatorName'
+        }
+    ],
+    'VolumeId': 'vol-21a0e7ea9b19f0043',
+    'Size': 8,
+    'State': "in-use"
+}
+
 SQS_MESSAGE_1 = {
     'account': 'core-services-dev',
     'account_id': '000000000000',
@@ -233,6 +261,41 @@
     'resources': [RESOURCE_1]
 }
 
+SQS_MESSAGE_5 = {
+    'account': 'core-services-dev',
+    'account_id': '000000000000',
+    'region': 'us-east-1',
+    'action': {
+        'to': ['slack://#test-channel'],
+        'template': 'default.html',
+        'type': 'notify',
+        'transport': {'queue': 'xxx', 'type': 'sqs'},
+        'subject': '{{ account }} AWS EBS Volumes will be DELETED in 15 DAYS!'
+    },
+    'policy': {
+        'filters': [{'Attachments': []}, {'tag:maid_status': 'absent'}],
+        'resource': 'ebs',
+        'actions': [
+            {
+                'type': 'mark-for-op',
+                'days': 15,
+                'op': 'delete'
+            },
+            {
+                'to': ['slack://tag/SlackChannel'],
+                'template': 'slack_default.j2',
+                'type': 'notify',
+                'subject': 'EBS Volumes will be DELETED in 15 DAYS!'
+            }
+        ],
+        'comments': 'We are deleting your EBS volumes.',
+        'name': 'ebs-mark-unattached-deletion'
+    },
+    'event': None,
+    'resources': [RESOURCE_3]
+}
+
+
 ASQ_MESSAGE = '''{
    "account":"subscription",
    "account_id":"ee98974b-5d2a-4d98-a78a-382f3715d07e",
diff --git a/tools/c7n_mailer/tests/test-templates/slack_default.j2 b/tools/c7n_mailer/tests/test-templates/slack_default.j2
new file mode 100644
--- /dev/null
+++ b/tools/c7n_mailer/tests/test-templates/slack_default.j2
@@ -0,0 +1,37 @@
+{
+   "attachments":[
+      {
+         "fallback":"Cloud Custodian Policy Violation",
+         "title":"Custodian",
+         "color":"danger",
+         "fields":[
+            {
+               "title":"Resources",
+               "value":"{%- for resource in resources -%}
+                        {{ format_resource(resource, policy['resource']) | replace('\\"', '"') | replace('"', '\\"') }}
+                        {%- endfor -%}"
+            },
+            {
+               "title":"Account",
+               "value":"{{ account }}"
+            },
+            {
+               "title":"Region",
+               "value":"{{ region }}"
+            },
+            {
+               "title":"Violation Description",
+               "value":"{{ action['violation_desc'] }}"
+            },
+            {
+               "title":"Action Description",
+               "value":"{{ action['action_desc'] }}"
+            }
+         ]
+      }
+   ],
+   {%- if not recipient.startswith('https://') %}
+   "channel":"{{ recipient }}",
+   {%- endif -%}
+   "username":"cloud-custodian"
+}
diff --git a/tools/c7n_mailer/tests/test_slack.py b/tools/c7n_mailer/tests/test_slack.py
new file mode 100644
--- /dev/null
+++ b/tools/c7n_mailer/tests/test_slack.py
@@ -0,0 +1,167 @@
+import unittest
+import copy
+import json
+import os
+from mock import patch, MagicMock
+
+from common import RESOURCE_3, SQS_MESSAGE_5
+
+from c7n_mailer.slack_delivery import SlackDelivery
+from c7n_mailer.email_delivery import EmailDelivery
+
+SLACK_TOKEN = "slack-token"
+SLACK_POST_MESSAGE_API = "https://slack.com/api/chat.postMessage"
+
+
+class TestSlackDelivery(unittest.TestCase):
+    def setUp(self):
+        self.config = {
+            'slack_token': SLACK_TOKEN,
+            'templates_folders': [
+                os.path.abspath(os.path.dirname(__file__)),
+                os.path.abspath('/'),
+                os.path.join(os.path.abspath(os.path.dirname(__file__)), "test-templates/")
+            ]
+        }
+
+        self.session = MagicMock()
+        self.logger = MagicMock()
+
+        self.email_delivery = EmailDelivery(self.config, self.session, self.logger)
+        self.message = copy.deepcopy(SQS_MESSAGE_5)
+        self.resource = copy.deepcopy(RESOURCE_3)
+        self.message['resources'] = [self.resource]
+        self.target_channel = 'test-channel'
+
+    def test_map_sending_to_channel(self):
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+
+        assert self.target_channel in result
+        assert json.loads(result[self.target_channel])['channel'] == self.target_channel
+
+    def test_map_sending_to_tag_channel(self):
+        self.target_channel = 'tag-channel'
+        print(self.config['templates_folders'])
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        message_destination = ['slack://tag/SlackChannel']
+
+        self.resource['Tags'].append({"Key": "SlackChannel", "Value": self.target_channel})
+        self.message['action']['to'] = message_destination
+        self.message['policy']['actions'][1]['to'] = message_destination
+
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+
+        assert self.target_channel in result
+        assert json.loads(result[self.target_channel])['channel'] == self.target_channel
+        self.logger.debug.assert_called_with("Generating message for specified Slack channel.")
+
+    def test_map_sending_to_tag_channel_no_tag(self):
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+
+        message_destination = ['slack://tag/SlackChannel']
+        self.message['action']['to'] = message_destination
+        self.message['policy']['actions'][1]['to'] = message_destination
+
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+
+        assert result == {}
+        self.logger.debug.assert_called_with("No SlackChannel tag found in resource.")
+
+    def test_map_sending_to_webhook(self):
+        webhook = "https://hooks.slack.com/this-is-a-webhook"
+
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+
+        message_destination = [webhook]
+        self.message['action']['to'] = message_destination
+        self.message['policy']['actions'][1]['to'] = message_destination
+
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+
+        assert webhook in result
+        assert 'channel' not in json.loads(result[webhook])
+
+    @patch('c7n_mailer.slack_delivery.requests.post')
+    def test_slack_handler(self, mock_post):
+        mock_post.return_value.status_code = 200
+        mock_post.return_value.json.return_value = {'ok': True}
+
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+        slack.slack_handler(self.message, result)
+
+        self.logger.info.assert_called_with("Sending account:core-services-dev "
+                                            "policy:ebs-mark-unattached-deletion ebs:1 slack:slack"
+                                            "_default to test-channel")
+
+    @patch('c7n_mailer.slack_delivery.requests.post')
+    def test_send_slack_msg_webhook(self, mock_post):
+        mock_post.return_value.status_code = 200
+        mock_post.return_value.json.return_value = {'ok': True}
+
+        webhook = "https://hooks.slack.com/this-is-a-webhook"
+        message_destination = [webhook]
+
+        self.message['action']['to'] = message_destination
+        self.message['policy']['actions'][1]['to'] = message_destination
+
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+        slack.send_slack_msg(webhook, result[webhook])
+
+        args, kwargs = mock_post.call_args
+        assert webhook == kwargs['url']
+        assert kwargs['data'] == result[webhook]
+
+    @patch('c7n_mailer.slack_delivery.requests.post')
+    def test_send_slack_msg(self, mock_post):
+        mock_post.return_value.status_code = 200
+        mock_post.return_value.json.return_value = {'ok': True}
+
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+        slack.send_slack_msg(self.target_channel, result[self.target_channel])
+
+        args, kwargs = mock_post.call_args
+        assert self.target_channel == json.loads(kwargs['data'])['channel']
+        assert SLACK_POST_MESSAGE_API == kwargs['url']
+        assert kwargs['data'] == result[self.target_channel]
+
+    @patch('c7n_mailer.slack_delivery.requests.post')
+    def test_send_slack_msg_retry_after(self, mock_post):
+        retry_after_delay = 1
+        mock_post.return_value.status_code = 429
+        mock_post.return_value.headers = {'Retry-After': retry_after_delay}
+
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+        slack.send_slack_msg(self.target_channel, result[self.target_channel])
+
+        args, kwargs = mock_post.call_args
+        self.logger.info.assert_called_with("Slack API rate limiting. Waiting %d seconds",
+                                            retry_after_delay)
+
+    @patch('c7n_mailer.slack_delivery.requests.post')
+    def test_send_slack_msg_not_200_response(self, mock_post):
+        mock_post.return_value.status_code = 404
+        mock_post.return_value.text = "channel_not_found"
+
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+        slack.send_slack_msg(self.target_channel, result[self.target_channel])
+
+        self.logger.info.assert_called_with('Error in sending Slack message status:%s response: %s',
+                                            404, 'channel_not_found')
+
+    @patch('c7n_mailer.slack_delivery.requests.post')
+    def test_send_slack_msg_not_ok_response(self, mock_post):
+        mock_post.return_value.status_code = 200
+        mock_post.return_value.json.return_value = {'ok': False, 'error': "failed"}
+
+        slack = SlackDelivery(self.config, self.logger, self.email_delivery)
+        result = slack.get_to_addrs_slack_messages_map(self.message)
+        slack.send_slack_msg(self.target_channel, result[self.target_channel])
+
+        self.logger.info.assert_called_with('Error in sending Slack message. Status:%s, '
+                                            'response:%s', 200, 'failed')