|
| 1 | +from __future__ import absolute_import |
| 2 | + |
| 3 | +import json |
| 4 | +import os |
| 5 | +import yaml |
| 6 | +import six |
| 7 | + |
| 8 | +from ansible import __version__ as ANSIBLE_VERSION |
| 9 | + |
| 10 | +from ansible_runner.interface import init_runner |
| 11 | + |
| 12 | +import pytest |
| 13 | + |
| 14 | + |
| 15 | +@pytest.fixture() |
| 16 | +def executor(tmpdir, request): |
| 17 | + private_data_dir = six.text_type(tmpdir.mkdir('foo')) |
| 18 | + |
| 19 | + playbooks = request.node.callspec.params.get('playbook') |
| 20 | + playbook = list(playbooks.values())[0] |
| 21 | + |
| 22 | + r = init_runner( |
| 23 | + private_data_dir=private_data_dir, |
| 24 | + inventory="localhost ansible_connection=local", |
| 25 | + playbook=yaml.safe_load(playbook) |
| 26 | + ) |
| 27 | + |
| 28 | + return r |
| 29 | + |
| 30 | + |
| 31 | +@pytest.mark.parametrize('event', {'playbook_on_start', |
| 32 | + 'playbook_on_play_start', |
| 33 | + 'playbook_on_task_start', 'runner_on_ok', |
| 34 | + 'playbook_on_stats'}) |
| 35 | +@pytest.mark.parametrize('playbook', [ |
| 36 | +{'helloworld.yml': ''' |
| 37 | +- name: Hello World Sample |
| 38 | + connection: local |
| 39 | + hosts: all |
| 40 | + gather_facts: no |
| 41 | + tasks: |
| 42 | + - name: Hello Message |
| 43 | + debug: |
| 44 | + msg: "Hello World!" |
| 45 | +'''}, # noqa |
| 46 | +{'results_included.yml': ''' |
| 47 | +- name: Run module which generates results list |
| 48 | + connection: local |
| 49 | + hosts: all |
| 50 | + gather_facts: no |
| 51 | + vars: |
| 52 | + results: ['foo', 'bar'] |
| 53 | + tasks: |
| 54 | + - name: Generate results list |
| 55 | + debug: |
| 56 | + var: results |
| 57 | +'''}, # noqa |
| 58 | +]) |
| 59 | +def test_callback_plugin_receives_events(executor, event, playbook): |
| 60 | + executor.run() |
| 61 | + assert len(list(executor.events)) |
| 62 | + assert event in [task['event'] for task in executor.events] |
| 63 | + |
| 64 | + |
| 65 | + |
| 66 | +@pytest.mark.parametrize('playbook', [ |
| 67 | +{'no_log_on_ok.yml': ''' |
| 68 | +- name: args should not be logged when task-level no_log is set |
| 69 | + connection: local |
| 70 | + hosts: all |
| 71 | + gather_facts: no |
| 72 | + tasks: |
| 73 | + - shell: echo "SENSITIVE" |
| 74 | + no_log: true |
| 75 | +'''}, # noqa |
| 76 | +{'no_log_on_fail.yml': ''' |
| 77 | +- name: failed args should not be logged when task-level no_log is set |
| 78 | + connection: local |
| 79 | + hosts: all |
| 80 | + gather_facts: no |
| 81 | + tasks: |
| 82 | + - shell: echo "SENSITIVE" |
| 83 | + no_log: true |
| 84 | + failed_when: true |
| 85 | + ignore_errors: true |
| 86 | +'''}, # noqa |
| 87 | +{'no_log_on_skip.yml': ''' |
| 88 | +- name: skipped task args should be suppressed with no_log |
| 89 | + connection: local |
| 90 | + hosts: all |
| 91 | + gather_facts: no |
| 92 | + tasks: |
| 93 | + - shell: echo "SENSITIVE" |
| 94 | + no_log: true |
| 95 | + when: false |
| 96 | +'''}, # noqa |
| 97 | +{'no_log_on_play.yml': ''' |
| 98 | +- name: args should not be logged when play-level no_log set |
| 99 | + connection: local |
| 100 | + hosts: all |
| 101 | + gather_facts: no |
| 102 | + no_log: true |
| 103 | + tasks: |
| 104 | + - shell: echo "SENSITIVE" |
| 105 | +'''}, # noqa |
| 106 | +{'async_no_log.yml': ''' |
| 107 | +- name: async task args should suppressed with no_log |
| 108 | + connection: local |
| 109 | + hosts: all |
| 110 | + gather_facts: no |
| 111 | + no_log: true |
| 112 | + tasks: |
| 113 | + - async: 10 |
| 114 | + poll: 1 |
| 115 | + shell: echo "SENSITIVE" |
| 116 | + no_log: true |
| 117 | +'''}, # noqa |
| 118 | +{'with_items.yml': ''' |
| 119 | +- name: with_items tasks should be suppressed with no_log |
| 120 | + connection: local |
| 121 | + hosts: all |
| 122 | + gather_facts: no |
| 123 | + tasks: |
| 124 | + - shell: echo {{ item }} |
| 125 | + no_log: true |
| 126 | + with_items: [ "SENSITIVE", "SENSITIVE-SKIPPED", "SENSITIVE-FAILED" ] |
| 127 | + when: item != "SENSITIVE-SKIPPED" |
| 128 | + failed_when: item == "SENSITIVE-FAILED" |
| 129 | + ignore_errors: yes |
| 130 | +'''}, # noqa, NOTE: with_items will be deprecated in 2.9 |
| 131 | +{'loop.yml': ''' |
| 132 | +- name: loop tasks should be suppressed with no_log |
| 133 | + connection: local |
| 134 | + hosts: all |
| 135 | + gather_facts: no |
| 136 | + tasks: |
| 137 | + - shell: echo {{ item }} |
| 138 | + no_log: true |
| 139 | + loop: [ "SENSITIVE", "SENSITIVE-SKIPPED", "SENSITIVE-FAILED" ] |
| 140 | + when: item != "SENSITIVE-SKIPPED" |
| 141 | + failed_when: item == "SENSITIVE-FAILED" |
| 142 | + ignore_errors: yes |
| 143 | +'''}, # noqa |
| 144 | +]) |
| 145 | +def test_callback_plugin_no_log_filters(executor, playbook): |
| 146 | + executor.run() |
| 147 | + assert len(list(executor.events)) |
| 148 | + assert 'SENSITIVE' not in json.dumps(list(executor.events)) |
| 149 | + |
| 150 | + |
| 151 | +@pytest.mark.parametrize('playbook', [ |
| 152 | +{'no_log_on_ok.yml': ''' |
| 153 | +- name: args should not be logged when no_log is set at the task or module level |
| 154 | + connection: local |
| 155 | + hosts: all |
| 156 | + gather_facts: no |
| 157 | + tasks: |
| 158 | + - shell: echo "PUBLIC" |
| 159 | + - shell: echo "PRIVATE" |
| 160 | + no_log: true |
| 161 | + - uri: url=https://example.org username="PUBLIC" password="PRIVATE" |
| 162 | + - copy: content="PRIVATE" dest="/tmp/tmp_no_log" |
| 163 | +'''}, # noqa |
| 164 | +]) |
| 165 | +def test_callback_plugin_task_args_leak(executor, playbook): |
| 166 | + executor.run() |
| 167 | + events = list(executor.events) |
| 168 | + assert events[0]['event'] == 'playbook_on_start' |
| 169 | + assert events[1]['event'] == 'playbook_on_play_start' |
| 170 | + |
| 171 | + # task 1 |
| 172 | + assert events[2]['event'] == 'playbook_on_task_start' |
| 173 | + assert events[3]['event'] == 'runner_on_ok' |
| 174 | + |
| 175 | + # task 2 no_log=True |
| 176 | + assert events[4]['event'] == 'playbook_on_task_start' |
| 177 | + assert events[5]['event'] == 'runner_on_ok' |
| 178 | + assert 'PUBLIC' in json.dumps(events) |
| 179 | + assert 'PRIVATE' not in json.dumps(events) |
| 180 | + # make sure playbook was successful, so all tasks were hit |
| 181 | + assert not events[-1]['event_data']['failures'], 'Unexpected playbook execution failure' |
| 182 | + |
| 183 | + |
| 184 | +@pytest.mark.parametrize('playbook', [ |
| 185 | +{'loop_with_no_log.yml': ''' |
| 186 | +- name: playbook variable should not be overwritten when using no log |
| 187 | + connection: local |
| 188 | + hosts: all |
| 189 | + gather_facts: no |
| 190 | + tasks: |
| 191 | + - command: "{{ item }}" |
| 192 | + register: command_register |
| 193 | + no_log: True |
| 194 | + with_items: |
| 195 | + - "echo helloworld!" |
| 196 | + - debug: msg="{{ command_register.results|map(attribute='stdout')|list }}" |
| 197 | +'''}, # noqa |
| 198 | +]) |
| 199 | +def test_callback_plugin_censoring_does_not_overwrite(executor, playbook): |
| 200 | + executor.run() |
| 201 | + events = list(executor.events) |
| 202 | + assert events[0]['event'] == 'playbook_on_start' |
| 203 | + assert events[1]['event'] == 'playbook_on_play_start' |
| 204 | + |
| 205 | + # task 1 |
| 206 | + assert events[2]['event'] == 'playbook_on_task_start' |
| 207 | + # Ordering of task and item events may differ randomly |
| 208 | + assert set(['runner_on_ok', 'runner_item_on_ok']) == set([data['event'] for data in events[3:5]]) |
| 209 | + |
| 210 | + # task 2 no_log=True |
| 211 | + assert events[5]['event'] == 'playbook_on_task_start' |
| 212 | + assert events[6]['event'] == 'runner_on_ok' |
| 213 | + assert 'helloworld!' in events[6]['event_data']['res']['msg'] |
| 214 | + |
| 215 | + |
| 216 | +@pytest.mark.parametrize('playbook', [ |
| 217 | +{'strip_env_vars.yml': ''' |
| 218 | +- name: sensitive environment variables should be stripped from events |
| 219 | + connection: local |
| 220 | + hosts: all |
| 221 | + tasks: |
| 222 | + - shell: echo "Hello, World!" |
| 223 | +'''}, # noqa |
| 224 | +]) |
| 225 | +def test_callback_plugin_strips_task_environ_variables(executor, playbook): |
| 226 | + executor.run() |
| 227 | + assert len(list(executor.events)) |
| 228 | + for event in list(executor.events): |
| 229 | + assert os.environ['PATH'] not in json.dumps(event) |
| 230 | + |
| 231 | + |
| 232 | +@pytest.mark.parametrize('playbook', [ |
| 233 | +{'custom_set_stat.yml': ''' |
| 234 | +- name: custom set_stat calls should persist to the local disk so awx can save them |
| 235 | + connection: local |
| 236 | + hosts: all |
| 237 | + tasks: |
| 238 | + - set_stats: |
| 239 | + data: |
| 240 | + foo: "bar" |
| 241 | +'''}, # noqa |
| 242 | +]) |
| 243 | +def test_callback_plugin_saves_custom_stats(executor, playbook): |
| 244 | + executor.run() |
| 245 | + for event in executor.events: |
| 246 | + event_data = event.get('event_data', {}) |
| 247 | + if 'artifact_data' in event_data: |
| 248 | + assert event_data['artifact_data'] == {'foo': 'bar'} |
| 249 | + break |
| 250 | + else: |
| 251 | + raise Exception('Did not find expected artifact data in event data') |
| 252 | + |
| 253 | + |
| 254 | +@pytest.mark.parametrize('playbook', [ |
| 255 | +{'handle_playbook_on_notify.yml': ''' |
| 256 | +- name: handle playbook_on_notify events properly |
| 257 | + connection: local |
| 258 | + hosts: all |
| 259 | + handlers: |
| 260 | + - name: my_handler |
| 261 | + debug: msg="My Handler" |
| 262 | + tasks: |
| 263 | + - debug: msg="My Task" |
| 264 | + changed_when: true |
| 265 | + notify: |
| 266 | + - my_handler |
| 267 | +'''}, # noqa |
| 268 | +]) |
| 269 | +@pytest.mark.skipif(ANSIBLE_VERSION < '2.5', reason="v2_playbook_on_notify doesn't work before ansible 2.5") |
| 270 | +def test_callback_plugin_records_notify_events(executor, playbook): |
| 271 | + executor.run() |
| 272 | + assert len(list(executor.events)) |
| 273 | + notify_events = [x for x in executor.events if x['event'] == 'playbook_on_notify'] |
| 274 | + assert len(notify_events) == 1 |
| 275 | + assert notify_events[0]['event_data']['handler'] == 'my_handler' |
| 276 | + assert notify_events[0]['event_data']['host'] == 'localhost' |
| 277 | + assert notify_events[0]['event_data']['task'] == 'debug' |
| 278 | + |
| 279 | + |
| 280 | +@pytest.mark.parametrize('playbook', [ |
| 281 | +{'no_log_module_with_var.yml': ''' |
| 282 | +- name: ensure that module-level secrets are redacted |
| 283 | + connection: local |
| 284 | + hosts: all |
| 285 | + vars: |
| 286 | + - pw: SENSITIVE |
| 287 | + tasks: |
| 288 | + - uri: |
| 289 | + url: https://example.org |
| 290 | + user: john-jacob-jingleheimer-schmidt |
| 291 | + password: "{{ pw }}" |
| 292 | +'''}, # noqa |
| 293 | +]) |
| 294 | +def test_module_level_no_log(executor, playbook): |
| 295 | + # It's possible for `no_log=True` to be defined at the _module_ level, |
| 296 | + # e.g., for the URI module password parameter |
| 297 | + # This test ensures that we properly redact those |
| 298 | + executor.run() |
| 299 | + assert len(list(executor.events)) |
| 300 | + assert 'john-jacob-jingleheimer-schmidt' in json.dumps(list(executor.events)) |
| 301 | + assert 'SENSITIVE' not in json.dumps(list(executor.events)) |
0 commit comments