We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Health check callback plugin causes warning on unhashable value
#### Description

Traceback when there was a random failure in logging:

```
roles/openshift_health_checker/callback_plugins/zz_failure_summary.py", line 147, in deduplicate_failures
    groups[group_key].append(failure)
TypeError: unhashable type: 'dict'
```

This is from the failure de-duplication code in the callback plugin. The result came back with a dict for a value and it won't hash.

##### Version

master

##### Observed Results

```
$ ansible-playbook playbooks/byo/config.yml -vvv
...
TASK [openshift_logging_elasticsearch : Set ES configmap] *********************************************************************************************************************************************************
task path: /home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_logging_elasticsearch/tasks/main.yaml:147
Wednesday 06 September 2017  09:52:01 -0400 (0:00:00.031)       0:16:19.770 *** 
Using module file /home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/lib_openshift/library/oc_configmap.py
<ec2-54-172-162-93.compute-1.amazonaws.com> (1, '\n{"msg": {"returncode": 1, "cmd": "/bin/oc create configmap logging-elasticsearch --from-file=elasticsearch.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml --from-file=logging.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml -n logging", "results": {}, "stderr": "Error from server: timeout\\n", "stdout": ""}, "failed": true, "invocation": {"module_args": {"from_file": {"elasticsearch.yml": "/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml", "logging.yml": "/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml"}, "name": "logging-elasticsearch", "from_literal": null, "namespace": "logging", "kubeconfig": "/etc/origin/master/admin.kubeconfig", "state": "present", "debug": false}}}\n', '')
fatal: [ec2-54-172-162-93.compute-1.amazonaws.com]: FAILED! => {
    "changed": false, 
    "failed": true, 
    "invocation": {
        "module_args": {
            "debug": false, 
            "from_file": {
                "elasticsearch.yml": "/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml", 
                "logging.yml": "/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml"
            }, 
            "from_literal": null, 
            "kubeconfig": "/etc/origin/master/admin.kubeconfig", 
            "name": "logging-elasticsearch", 
            "namespace": "logging", 
            "state": "present"
        }
    }
}

MSG:

{u'returncode': 1, u'cmd': u'/bin/oc create configmap logging-elasticsearch --from-file=elasticsearch.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml --from-file=logging.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml -n logging', u'results': {}, u'stderr': u'Error from server: timeout\n', u'stdout': u''}

...
An error happened while generating a summary of failures:
Traceback (most recent call last):
  File "/home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py", line 48, in v2_playbook_on_stats
    self._display.display(failure_summary(self.__failures, self.__playbook_file))
  File "/home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py", line 74, in failure_summary
    failures = deduplicate_failures(failures)
  File "/home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py", line 147, in deduplicate_failures
    groups[group_key].append(failure)
TypeError: unhashable type: 'dict'

```

##### Additional Information
This isn't the cause of whatever failed, it's just adding to the confusion with a warning at the end.

I think it's the "msg" being returned in the task failure that it's trying to hash and failing. Callback plugin code needs to be more paranoid :)

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `roles/openshift_health_checker/callback_plugins/zz_failure_summary.py`
Content:
```
1 """Ansible callback plugin to print a nicely formatted summary of failures.
2 
3 The file / module name is prefixed with `zz_` to make this plugin be loaded last
4 by Ansible, thus making its output the last thing that users see.
5 """
6 
7 from collections import defaultdict
8 import traceback
9 
10 from ansible.plugins.callback import CallbackBase
11 from ansible import constants as C
12 from ansible.utils.color import stringc
13 
14 
15 FAILED_NO_MSG = u'Failed without returning a message.'
16 
17 
18 class CallbackModule(CallbackBase):
19     """This callback plugin stores task results and summarizes failures."""
20 
21     CALLBACK_VERSION = 2.0
22     CALLBACK_TYPE = 'aggregate'
23     CALLBACK_NAME = 'failure_summary'
24     CALLBACK_NEEDS_WHITELIST = False
25 
26     def __init__(self):
27         super(CallbackModule, self).__init__()
28         self.__failures = []
29         self.__playbook_file = ''
30 
31     def v2_playbook_on_start(self, playbook):
32         super(CallbackModule, self).v2_playbook_on_start(playbook)
33         # pylint: disable=protected-access; Ansible gives us no public API to
34         # get the file name of the current playbook from a callback plugin.
35         self.__playbook_file = playbook._file_name
36 
37     def v2_runner_on_failed(self, result, ignore_errors=False):
38         super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors)
39         if not ignore_errors:
40             self.__failures.append(result)
41 
42     def v2_playbook_on_stats(self, stats):
43         super(CallbackModule, self).v2_playbook_on_stats(stats)
44         # pylint: disable=broad-except; capturing exceptions broadly is
45         # intentional, to isolate arbitrary failures in this callback plugin.
46         try:
47             if self.__failures:
48                 self._display.display(failure_summary(self.__failures, self.__playbook_file))
49         except Exception:
50             msg = stringc(
51                 u'An error happened while generating a summary of failures:\n'
52                 u'{}'.format(traceback.format_exc()), C.COLOR_WARN)
53             self._display.v(msg)
54 
55 
56 def failure_summary(failures, playbook):
57     """Return a summary of failed tasks, including details on health checks."""
58     if not failures:
59         return u''
60 
61     # NOTE: because we don't have access to task_vars from callback plugins, we
62     # store the playbook context in the task result when the
63     # openshift_health_check action plugin is used, and we use this context to
64     # customize the error message.
65     # pylint: disable=protected-access; Ansible gives us no sufficient public
66     # API on TaskResult objects.
67     context = next((
68         context for context in
69         (failure._result.get('playbook_context') for failure in failures)
70         if context
71     ), None)
72 
73     failures = [failure_to_dict(failure) for failure in failures]
74     failures = deduplicate_failures(failures)
75 
76     summary = [u'', u'', u'Failure summary:', u'']
77 
78     width = len(str(len(failures)))
79     initial_indent_format = u'  {{:>{width}}}. '.format(width=width)
80     initial_indent_len = len(initial_indent_format.format(0))
81     subsequent_indent = u' ' * initial_indent_len
82     subsequent_extra_indent = u' ' * (initial_indent_len + 10)
83 
84     for i, failure in enumerate(failures, 1):
85         entries = format_failure(failure)
86         summary.append(u'\n{}{}'.format(initial_indent_format.format(i), entries[0]))
87         for entry in entries[1:]:
88             entry = entry.replace(u'\n', u'\n' + subsequent_extra_indent)
89             indented = u'{}{}'.format(subsequent_indent, entry)
90             summary.append(indented)
91 
92     failed_checks = set()
93     for failure in failures:
94         failed_checks.update(name for name, message in failure['checks'])
95     if failed_checks:
96         summary.append(check_failure_footer(failed_checks, context, playbook))
97 
98     return u'\n'.join(summary)
99 
100 
101 def failure_to_dict(failed_task_result):
102     """Extract information out of a failed TaskResult into a dict.
103 
104     The intent is to transform a TaskResult object into something easier to
105     manipulate. TaskResult is ansible.executor.task_result.TaskResult.
106     """
107     # pylint: disable=protected-access; Ansible gives us no sufficient public
108     # API on TaskResult objects.
109     _result = failed_task_result._result
110     return {
111         'host': failed_task_result._host.get_name(),
112         'play': play_name(failed_task_result._task),
113         'task': failed_task_result.task_name,
114         'msg': _result.get('msg', FAILED_NO_MSG),
115         'checks': tuple(
116             (name, result.get('msg', FAILED_NO_MSG))
117             for name, result in sorted(_result.get('checks', {}).items())
118             if result.get('failed')
119         ),
120     }
121 
122 
123 def play_name(obj):
124     """Given a task or block, return the name of its parent play.
125 
126     This is loosely inspired by ansible.playbook.base.Base.dump_me.
127     """
128     # pylint: disable=protected-access; Ansible gives us no sufficient public
129     # API to implement this.
130     if not obj:
131         return ''
132     if hasattr(obj, '_play'):
133         return obj._play.get_name()
134     return play_name(getattr(obj, '_parent'))
135 
136 
137 def deduplicate_failures(failures):
138     """Group together similar failures from different hosts.
139 
140     Returns a new list of failures such that identical failures from different
141     hosts are grouped together in a single entry. The relative order of failures
142     is preserved.
143     """
144     groups = defaultdict(list)
145     for failure in failures:
146         group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))
147         groups[group_key].append(failure)
148     result = []
149     for failure in failures:
150         group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))
151         if group_key not in groups:
152             continue
153         failure['host'] = tuple(sorted(g_failure['host'] for g_failure in groups.pop(group_key)))
154         result.append(failure)
155     return result
156 
157 
158 def format_failure(failure):
159     """Return a list of pretty-formatted text entries describing a failure, including
160     relevant information about it. Expect that the list of text entries will be joined
161     by a newline separator when output to the user."""
162     host = u', '.join(failure['host'])
163     play = failure['play']
164     task = failure['task']
165     msg = failure['msg']
166     checks = failure['checks']
167     fields = (
168         (u'Hosts', host),
169         (u'Play', play),
170         (u'Task', task),
171         (u'Message', stringc(msg, C.COLOR_ERROR)),
172     )
173     if checks:
174         fields += ((u'Details', format_failed_checks(checks)),)
175     row_format = '{:10}{}'
176     return [row_format.format(header + u':', body) for header, body in fields]
177 
178 
179 def format_failed_checks(checks):
180     """Return pretty-formatted text describing checks that failed."""
181     messages = []
182     for name, message in checks:
183         messages.append(u'check "{}":\n{}'.format(name, message))
184     return stringc(u'\n\n'.join(messages), C.COLOR_ERROR)
185 
186 
187 def check_failure_footer(failed_checks, context, playbook):
188     """Return a textual explanation about checks depending on context.
189 
190     The purpose of specifying context is to vary the output depending on what
191     the user was expecting to happen (based on which playbook they ran). The
192     only use currently is to vary the message depending on whether the user was
193     deliberately running checks or was trying to install/upgrade and checks are
194     just included. Other use cases may arise.
195     """
196     checks = ','.join(sorted(failed_checks))
197     summary = [u'']
198     if context in ['pre-install', 'health', 'adhoc']:
199         # User was expecting to run checks, less explanation needed.
200         summary.extend([
201             u'You may configure or disable checks by setting Ansible '
202             u'variables. To disable those above, set:',
203             u'    openshift_disable_check={checks}'.format(checks=checks),
204             u'Consult check documentation for configurable variables.',
205         ])
206     else:
207         # User may not be familiar with the checks, explain what checks are in
208         # the first place.
209         summary.extend([
210             u'The execution of "{playbook}" includes checks designed to fail '
211             u'early if the requirements of the playbook are not met. One or '
212             u'more of these checks failed. To disregard these results,'
213             u'explicitly disable checks by setting an Ansible variable:'.format(playbook=playbook),
214             u'   openshift_disable_check={checks}'.format(checks=checks),
215             u'Failing check names are shown in the failure details above. '
216             u'Some checks may be configurable by variables if your requirements '
217             u'are different from the defaults; consult check documentation.',
218         ])
219     summary.append(
220         u'Variables can be set in the inventory or passed on the command line '
221         u'using the -e flag to ansible-playbook.'
222     )
223     return u'\n'.join(summary)
224
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py b/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py
--- a/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py
+++ b/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py
@@ -10,6 +10,7 @@
 from ansible.plugins.callback import CallbackBase
 from ansible import constants as C
 from ansible.utils.color import stringc
+from ansible.module_utils.six import string_types
 
 
 FAILED_NO_MSG = u'Failed without returning a message.'
@@ -140,11 +141,19 @@
     Returns a new list of failures such that identical failures from different
     hosts are grouped together in a single entry. The relative order of failures
     is preserved.
+
+    If failures is unhashable, the original list of failures is returned.
     """
     groups = defaultdict(list)
     for failure in failures:
         group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))
-        groups[group_key].append(failure)
+        try:
+            groups[group_key].append(failure)
+        except TypeError:
+            # abort and return original list of failures when failures has an
+            # unhashable type.
+            return failures
+
     result = []
     for failure in failures:
         group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))
@@ -159,7 +168,10 @@
     """Return a list of pretty-formatted text entries describing a failure, including
     relevant information about it. Expect that the list of text entries will be joined
     by a newline separator when output to the user."""
-    host = u', '.join(failure['host'])
+    if isinstance(failure['host'], string_types):
+        host = failure['host']
+    else:
+        host = u', '.join(failure['host'])
     play = failure['play']
     task = failure['task']
     msg = failure['msg']

{"golden_diff": "diff --git a/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py b/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py\n--- a/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py\n+++ b/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py\n@@ -10,6 +10,7 @@\n from ansible.plugins.callback import CallbackBase\n from ansible import constants as C\n from ansible.utils.color import stringc\n+from ansible.module_utils.six import string_types\n \n \n FAILED_NO_MSG = u'Failed without returning a message.'\n@@ -140,11 +141,19 @@\n     Returns a new list of failures such that identical failures from different\n     hosts are grouped together in a single entry. The relative order of failures\n     is preserved.\n+\n+    If failures is unhashable, the original list of failures is returned.\n     \"\"\"\n     groups = defaultdict(list)\n     for failure in failures:\n         group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))\n-        groups[group_key].append(failure)\n+        try:\n+            groups[group_key].append(failure)\n+        except TypeError:\n+            # abort and return original list of failures when failures has an\n+            # unhashable type.\n+            return failures\n+\n     result = []\n     for failure in failures:\n         group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))\n@@ -159,7 +168,10 @@\n     \"\"\"Return a list of pretty-formatted text entries describing a failure, including\n     relevant information about it. Expect that the list of text entries will be joined\n     by a newline separator when output to the user.\"\"\"\n-    host = u', '.join(failure['host'])\n+    if isinstance(failure['host'], string_types):\n+        host = failure['host']\n+    else:\n+        host = u', '.join(failure['host'])\n     play = failure['play']\n     task = failure['task']\n     msg = failure['msg']\n", "issue": "Health check callback plugin causes warning on unhashable value\n#### Description\r\n\r\nTraceback when there was a random failure in logging:\r\n\r\n```\r\nroles/openshift_health_checker/callback_plugins/zz_failure_summary.py\", line 147, in deduplicate_failures\r\n    groups[group_key].append(failure)\r\nTypeError: unhashable type: 'dict'\r\n```\r\n\r\nThis is from the failure de-duplication code in the callback plugin. The result came back with a dict for a value and it won't hash.\r\n\r\n##### Version\r\n\r\nmaster\r\n\r\n##### Observed Results\r\n\r\n```\r\n$ ansible-playbook playbooks/byo/config.yml -vvv\r\n...\r\nTASK [openshift_logging_elasticsearch : Set ES configmap] *********************************************************************************************************************************************************\r\ntask path: /home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_logging_elasticsearch/tasks/main.yaml:147\r\nWednesday 06 September 2017  09:52:01 -0400 (0:00:00.031)       0:16:19.770 *** \r\nUsing module file /home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/lib_openshift/library/oc_configmap.py\r\n<ec2-54-172-162-93.compute-1.amazonaws.com> (1, '\\n{\"msg\": {\"returncode\": 1, \"cmd\": \"/bin/oc create configmap logging-elasticsearch --from-file=elasticsearch.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml --from-file=logging.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml -n logging\", \"results\": {}, \"stderr\": \"Error from server: timeout\\\\n\", \"stdout\": \"\"}, \"failed\": true, \"invocation\": {\"module_args\": {\"from_file\": {\"elasticsearch.yml\": \"/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml\", \"logging.yml\": \"/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml\"}, \"name\": \"logging-elasticsearch\", \"from_literal\": null, \"namespace\": \"logging\", \"kubeconfig\": \"/etc/origin/master/admin.kubeconfig\", \"state\": \"present\", \"debug\": false}}}\\n', '')\r\nfatal: [ec2-54-172-162-93.compute-1.amazonaws.com]: FAILED! => {\r\n    \"changed\": false, \r\n    \"failed\": true, \r\n    \"invocation\": {\r\n        \"module_args\": {\r\n            \"debug\": false, \r\n            \"from_file\": {\r\n                \"elasticsearch.yml\": \"/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml\", \r\n                \"logging.yml\": \"/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml\"\r\n            }, \r\n            \"from_literal\": null, \r\n            \"kubeconfig\": \"/etc/origin/master/admin.kubeconfig\", \r\n            \"name\": \"logging-elasticsearch\", \r\n            \"namespace\": \"logging\", \r\n            \"state\": \"present\"\r\n        }\r\n    }\r\n}\r\n\r\nMSG:\r\n\r\n{u'returncode': 1, u'cmd': u'/bin/oc create configmap logging-elasticsearch --from-file=elasticsearch.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch.yml --from-file=logging.yml=/tmp/openshift-logging-ansible-C6rjd2/elasticsearch-logging.yml -n logging', u'results': {}, u'stderr': u'Error from server: timeout\\n', u'stdout': u''}\r\n\r\n...\r\nAn error happened while generating a summary of failures:\r\nTraceback (most recent call last):\r\n  File \"/home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py\", line 48, in v2_playbook_on_stats\r\n    self._display.display(failure_summary(self.__failures, self.__playbook_file))\r\n  File \"/home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py\", line 74, in failure_summary\r\n    failures = deduplicate_failures(failures)\r\n  File \"/home/lmeyer/go/src/github.com/openshift/openshift-ansible/roles/openshift_health_checker/callback_plugins/zz_failure_summary.py\", line 147, in deduplicate_failures\r\n    groups[group_key].append(failure)\r\nTypeError: unhashable type: 'dict'\r\n\r\n```\r\n\r\n##### Additional Information\r\nThis isn't the cause of whatever failed, it's just adding to the confusion with a warning at the end.\r\n\r\nI think it's the \"msg\" being returned in the task failure that it's trying to hash and failing. Callback plugin code needs to be more paranoid :)\n", "before_files": [{"content": "\"\"\"Ansible callback plugin to print a nicely formatted summary of failures.\n\nThe file / module name is prefixed with `zz_` to make this plugin be loaded last\nby Ansible, thus making its output the last thing that users see.\n\"\"\"\n\nfrom collections import defaultdict\nimport traceback\n\nfrom ansible.plugins.callback import CallbackBase\nfrom ansible import constants as C\nfrom ansible.utils.color import stringc\n\n\nFAILED_NO_MSG = u'Failed without returning a message.'\n\n\nclass CallbackModule(CallbackBase):\n    \"\"\"This callback plugin stores task results and summarizes failures.\"\"\"\n\n    CALLBACK_VERSION = 2.0\n    CALLBACK_TYPE = 'aggregate'\n    CALLBACK_NAME = 'failure_summary'\n    CALLBACK_NEEDS_WHITELIST = False\n\n    def __init__(self):\n        super(CallbackModule, self).__init__()\n        self.__failures = []\n        self.__playbook_file = ''\n\n    def v2_playbook_on_start(self, playbook):\n        super(CallbackModule, self).v2_playbook_on_start(playbook)\n        # pylint: disable=protected-access; Ansible gives us no public API to\n        # get the file name of the current playbook from a callback plugin.\n        self.__playbook_file = playbook._file_name\n\n    def v2_runner_on_failed(self, result, ignore_errors=False):\n        super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors)\n        if not ignore_errors:\n            self.__failures.append(result)\n\n    def v2_playbook_on_stats(self, stats):\n        super(CallbackModule, self).v2_playbook_on_stats(stats)\n        # pylint: disable=broad-except; capturing exceptions broadly is\n        # intentional, to isolate arbitrary failures in this callback plugin.\n        try:\n            if self.__failures:\n                self._display.display(failure_summary(self.__failures, self.__playbook_file))\n        except Exception:\n            msg = stringc(\n                u'An error happened while generating a summary of failures:\\n'\n                u'{}'.format(traceback.format_exc()), C.COLOR_WARN)\n            self._display.v(msg)\n\n\ndef failure_summary(failures, playbook):\n    \"\"\"Return a summary of failed tasks, including details on health checks.\"\"\"\n    if not failures:\n        return u''\n\n    # NOTE: because we don't have access to task_vars from callback plugins, we\n    # store the playbook context in the task result when the\n    # openshift_health_check action plugin is used, and we use this context to\n    # customize the error message.\n    # pylint: disable=protected-access; Ansible gives us no sufficient public\n    # API on TaskResult objects.\n    context = next((\n        context for context in\n        (failure._result.get('playbook_context') for failure in failures)\n        if context\n    ), None)\n\n    failures = [failure_to_dict(failure) for failure in failures]\n    failures = deduplicate_failures(failures)\n\n    summary = [u'', u'', u'Failure summary:', u'']\n\n    width = len(str(len(failures)))\n    initial_indent_format = u'  {{:>{width}}}. '.format(width=width)\n    initial_indent_len = len(initial_indent_format.format(0))\n    subsequent_indent = u' ' * initial_indent_len\n    subsequent_extra_indent = u' ' * (initial_indent_len + 10)\n\n    for i, failure in enumerate(failures, 1):\n        entries = format_failure(failure)\n        summary.append(u'\\n{}{}'.format(initial_indent_format.format(i), entries[0]))\n        for entry in entries[1:]:\n            entry = entry.replace(u'\\n', u'\\n' + subsequent_extra_indent)\n            indented = u'{}{}'.format(subsequent_indent, entry)\n            summary.append(indented)\n\n    failed_checks = set()\n    for failure in failures:\n        failed_checks.update(name for name, message in failure['checks'])\n    if failed_checks:\n        summary.append(check_failure_footer(failed_checks, context, playbook))\n\n    return u'\\n'.join(summary)\n\n\ndef failure_to_dict(failed_task_result):\n    \"\"\"Extract information out of a failed TaskResult into a dict.\n\n    The intent is to transform a TaskResult object into something easier to\n    manipulate. TaskResult is ansible.executor.task_result.TaskResult.\n    \"\"\"\n    # pylint: disable=protected-access; Ansible gives us no sufficient public\n    # API on TaskResult objects.\n    _result = failed_task_result._result\n    return {\n        'host': failed_task_result._host.get_name(),\n        'play': play_name(failed_task_result._task),\n        'task': failed_task_result.task_name,\n        'msg': _result.get('msg', FAILED_NO_MSG),\n        'checks': tuple(\n            (name, result.get('msg', FAILED_NO_MSG))\n            for name, result in sorted(_result.get('checks', {}).items())\n            if result.get('failed')\n        ),\n    }\n\n\ndef play_name(obj):\n    \"\"\"Given a task or block, return the name of its parent play.\n\n    This is loosely inspired by ansible.playbook.base.Base.dump_me.\n    \"\"\"\n    # pylint: disable=protected-access; Ansible gives us no sufficient public\n    # API to implement this.\n    if not obj:\n        return ''\n    if hasattr(obj, '_play'):\n        return obj._play.get_name()\n    return play_name(getattr(obj, '_parent'))\n\n\ndef deduplicate_failures(failures):\n    \"\"\"Group together similar failures from different hosts.\n\n    Returns a new list of failures such that identical failures from different\n    hosts are grouped together in a single entry. The relative order of failures\n    is preserved.\n    \"\"\"\n    groups = defaultdict(list)\n    for failure in failures:\n        group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))\n        groups[group_key].append(failure)\n    result = []\n    for failure in failures:\n        group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))\n        if group_key not in groups:\n            continue\n        failure['host'] = tuple(sorted(g_failure['host'] for g_failure in groups.pop(group_key)))\n        result.append(failure)\n    return result\n\n\ndef format_failure(failure):\n    \"\"\"Return a list of pretty-formatted text entries describing a failure, including\n    relevant information about it. Expect that the list of text entries will be joined\n    by a newline separator when output to the user.\"\"\"\n    host = u', '.join(failure['host'])\n    play = failure['play']\n    task = failure['task']\n    msg = failure['msg']\n    checks = failure['checks']\n    fields = (\n        (u'Hosts', host),\n        (u'Play', play),\n        (u'Task', task),\n        (u'Message', stringc(msg, C.COLOR_ERROR)),\n    )\n    if checks:\n        fields += ((u'Details', format_failed_checks(checks)),)\n    row_format = '{:10}{}'\n    return [row_format.format(header + u':', body) for header, body in fields]\n\n\ndef format_failed_checks(checks):\n    \"\"\"Return pretty-formatted text describing checks that failed.\"\"\"\n    messages = []\n    for name, message in checks:\n        messages.append(u'check \"{}\":\\n{}'.format(name, message))\n    return stringc(u'\\n\\n'.join(messages), C.COLOR_ERROR)\n\n\ndef check_failure_footer(failed_checks, context, playbook):\n    \"\"\"Return a textual explanation about checks depending on context.\n\n    The purpose of specifying context is to vary the output depending on what\n    the user was expecting to happen (based on which playbook they ran). The\n    only use currently is to vary the message depending on whether the user was\n    deliberately running checks or was trying to install/upgrade and checks are\n    just included. Other use cases may arise.\n    \"\"\"\n    checks = ','.join(sorted(failed_checks))\n    summary = [u'']\n    if context in ['pre-install', 'health', 'adhoc']:\n        # User was expecting to run checks, less explanation needed.\n        summary.extend([\n            u'You may configure or disable checks by setting Ansible '\n            u'variables. To disable those above, set:',\n            u'    openshift_disable_check={checks}'.format(checks=checks),\n            u'Consult check documentation for configurable variables.',\n        ])\n    else:\n        # User may not be familiar with the checks, explain what checks are in\n        # the first place.\n        summary.extend([\n            u'The execution of \"{playbook}\" includes checks designed to fail '\n            u'early if the requirements of the playbook are not met. One or '\n            u'more of these checks failed. To disregard these results,'\n            u'explicitly disable checks by setting an Ansible variable:'.format(playbook=playbook),\n            u'   openshift_disable_check={checks}'.format(checks=checks),\n            u'Failing check names are shown in the failure details above. '\n            u'Some checks may be configurable by variables if your requirements '\n            u'are different from the defaults; consult check documentation.',\n        ])\n    summary.append(\n        u'Variables can be set in the inventory or passed on the command line '\n        u'using the -e flag to ansible-playbook.'\n    )\n    return u'\\n'.join(summary)\n", "path": "roles/openshift_health_checker/callback_plugins/zz_failure_summary.py"}], "after_files": [{"content": "\"\"\"Ansible callback plugin to print a nicely formatted summary of failures.\n\nThe file / module name is prefixed with `zz_` to make this plugin be loaded last\nby Ansible, thus making its output the last thing that users see.\n\"\"\"\n\nfrom collections import defaultdict\nimport traceback\n\nfrom ansible.plugins.callback import CallbackBase\nfrom ansible import constants as C\nfrom ansible.utils.color import stringc\nfrom ansible.module_utils.six import string_types\n\n\nFAILED_NO_MSG = u'Failed without returning a message.'\n\n\nclass CallbackModule(CallbackBase):\n    \"\"\"This callback plugin stores task results and summarizes failures.\"\"\"\n\n    CALLBACK_VERSION = 2.0\n    CALLBACK_TYPE = 'aggregate'\n    CALLBACK_NAME = 'failure_summary'\n    CALLBACK_NEEDS_WHITELIST = False\n\n    def __init__(self):\n        super(CallbackModule, self).__init__()\n        self.__failures = []\n        self.__playbook_file = ''\n\n    def v2_playbook_on_start(self, playbook):\n        super(CallbackModule, self).v2_playbook_on_start(playbook)\n        # pylint: disable=protected-access; Ansible gives us no public API to\n        # get the file name of the current playbook from a callback plugin.\n        self.__playbook_file = playbook._file_name\n\n    def v2_runner_on_failed(self, result, ignore_errors=False):\n        super(CallbackModule, self).v2_runner_on_failed(result, ignore_errors)\n        if not ignore_errors:\n            self.__failures.append(result)\n\n    def v2_playbook_on_stats(self, stats):\n        super(CallbackModule, self).v2_playbook_on_stats(stats)\n        # pylint: disable=broad-except; capturing exceptions broadly is\n        # intentional, to isolate arbitrary failures in this callback plugin.\n        try:\n            if self.__failures:\n                self._display.display(failure_summary(self.__failures, self.__playbook_file))\n        except Exception:\n            msg = stringc(\n                u'An error happened while generating a summary of failures:\\n'\n                u'{}'.format(traceback.format_exc()), C.COLOR_WARN)\n            self._display.v(msg)\n\n\ndef failure_summary(failures, playbook):\n    \"\"\"Return a summary of failed tasks, including details on health checks.\"\"\"\n    if not failures:\n        return u''\n\n    # NOTE: because we don't have access to task_vars from callback plugins, we\n    # store the playbook context in the task result when the\n    # openshift_health_check action plugin is used, and we use this context to\n    # customize the error message.\n    # pylint: disable=protected-access; Ansible gives us no sufficient public\n    # API on TaskResult objects.\n    context = next((\n        context for context in\n        (failure._result.get('playbook_context') for failure in failures)\n        if context\n    ), None)\n\n    failures = [failure_to_dict(failure) for failure in failures]\n    failures = deduplicate_failures(failures)\n\n    summary = [u'', u'', u'Failure summary:', u'']\n\n    width = len(str(len(failures)))\n    initial_indent_format = u'  {{:>{width}}}. '.format(width=width)\n    initial_indent_len = len(initial_indent_format.format(0))\n    subsequent_indent = u' ' * initial_indent_len\n    subsequent_extra_indent = u' ' * (initial_indent_len + 10)\n\n    for i, failure in enumerate(failures, 1):\n        entries = format_failure(failure)\n        summary.append(u'\\n{}{}'.format(initial_indent_format.format(i), entries[0]))\n        for entry in entries[1:]:\n            entry = entry.replace(u'\\n', u'\\n' + subsequent_extra_indent)\n            indented = u'{}{}'.format(subsequent_indent, entry)\n            summary.append(indented)\n\n    failed_checks = set()\n    for failure in failures:\n        failed_checks.update(name for name, message in failure['checks'])\n    if failed_checks:\n        summary.append(check_failure_footer(failed_checks, context, playbook))\n\n    return u'\\n'.join(summary)\n\n\ndef failure_to_dict(failed_task_result):\n    \"\"\"Extract information out of a failed TaskResult into a dict.\n\n    The intent is to transform a TaskResult object into something easier to\n    manipulate. TaskResult is ansible.executor.task_result.TaskResult.\n    \"\"\"\n    # pylint: disable=protected-access; Ansible gives us no sufficient public\n    # API on TaskResult objects.\n    _result = failed_task_result._result\n    return {\n        'host': failed_task_result._host.get_name(),\n        'play': play_name(failed_task_result._task),\n        'task': failed_task_result.task_name,\n        'msg': _result.get('msg', FAILED_NO_MSG),\n        'checks': tuple(\n            (name, result.get('msg', FAILED_NO_MSG))\n            for name, result in sorted(_result.get('checks', {}).items())\n            if result.get('failed')\n        ),\n    }\n\n\ndef play_name(obj):\n    \"\"\"Given a task or block, return the name of its parent play.\n\n    This is loosely inspired by ansible.playbook.base.Base.dump_me.\n    \"\"\"\n    # pylint: disable=protected-access; Ansible gives us no sufficient public\n    # API to implement this.\n    if not obj:\n        return ''\n    if hasattr(obj, '_play'):\n        return obj._play.get_name()\n    return play_name(getattr(obj, '_parent'))\n\n\ndef deduplicate_failures(failures):\n    \"\"\"Group together similar failures from different hosts.\n\n    Returns a new list of failures such that identical failures from different\n    hosts are grouped together in a single entry. The relative order of failures\n    is preserved.\n\n    If failures is unhashable, the original list of failures is returned.\n    \"\"\"\n    groups = defaultdict(list)\n    for failure in failures:\n        group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))\n        try:\n            groups[group_key].append(failure)\n        except TypeError:\n            # abort and return original list of failures when failures has an\n            # unhashable type.\n            return failures\n\n    result = []\n    for failure in failures:\n        group_key = tuple(sorted((key, value) for key, value in failure.items() if key != 'host'))\n        if group_key not in groups:\n            continue\n        failure['host'] = tuple(sorted(g_failure['host'] for g_failure in groups.pop(group_key)))\n        result.append(failure)\n    return result\n\n\ndef format_failure(failure):\n    \"\"\"Return a list of pretty-formatted text entries describing a failure, including\n    relevant information about it. Expect that the list of text entries will be joined\n    by a newline separator when output to the user.\"\"\"\n    if isinstance(failure['host'], string_types):\n        host = failure['host']\n    else:\n        host = u', '.join(failure['host'])\n    play = failure['play']\n    task = failure['task']\n    msg = failure['msg']\n    checks = failure['checks']\n    fields = (\n        (u'Hosts', host),\n        (u'Play', play),\n        (u'Task', task),\n        (u'Message', stringc(msg, C.COLOR_ERROR)),\n    )\n    if checks:\n        fields += ((u'Details', format_failed_checks(checks)),)\n    row_format = '{:10}{}'\n    return [row_format.format(header + u':', body) for header, body in fields]\n\n\ndef format_failed_checks(checks):\n    \"\"\"Return pretty-formatted text describing checks that failed.\"\"\"\n    messages = []\n    for name, message in checks:\n        messages.append(u'check \"{}\":\\n{}'.format(name, message))\n    return stringc(u'\\n\\n'.join(messages), C.COLOR_ERROR)\n\n\ndef check_failure_footer(failed_checks, context, playbook):\n    \"\"\"Return a textual explanation about checks depending on context.\n\n    The purpose of specifying context is to vary the output depending on what\n    the user was expecting to happen (based on which playbook they ran). The\n    only use currently is to vary the message depending on whether the user was\n    deliberately running checks or was trying to install/upgrade and checks are\n    just included. Other use cases may arise.\n    \"\"\"\n    checks = ','.join(sorted(failed_checks))\n    summary = [u'']\n    if context in ['pre-install', 'health', 'adhoc']:\n        # User was expecting to run checks, less explanation needed.\n        summary.extend([\n            u'You may configure or disable checks by setting Ansible '\n            u'variables. To disable those above, set:',\n            u'    openshift_disable_check={checks}'.format(checks=checks),\n            u'Consult check documentation for configurable variables.',\n        ])\n    else:\n        # User may not be familiar with the checks, explain what checks are in\n        # the first place.\n        summary.extend([\n            u'The execution of \"{playbook}\" includes checks designed to fail '\n            u'early if the requirements of the playbook are not met. One or '\n            u'more of these checks failed. To disregard these results,'\n            u'explicitly disable checks by setting an Ansible variable:'.format(playbook=playbook),\n            u'   openshift_disable_check={checks}'.format(checks=checks),\n            u'Failing check names are shown in the failure details above. '\n            u'Some checks may be configurable by variables if your requirements '\n            u'are different from the defaults; consult check documentation.',\n        ])\n    summary.append(\n        u'Variables can be set in the inventory or passed on the command line '\n        u'using the -e flag to ansible-playbook.'\n    )\n    return u'\\n'.join(summary)\n", "path": "roles/openshift_health_checker/callback_plugins/zz_failure_summary.py"}]}