From 98ee3e177a5f54f6ced93ea159d172d9f8c921b4 Mon Sep 17 00:00:00 2001 From: David Wilson Date: Mon, 9 Apr 2018 15:04:40 +0100 Subject: [PATCH] ansible: tests for sudo behaviour; closes #143. --- ansible_mitogen/connection.py | 43 ++++++++++++---- tests/ansible/integration/all.yml | 1 + tests/ansible/integration/become/all.yml | 6 +++ .../integration/become/sudo_flags_failure.yml | 23 +++++++++ .../integration/become/sudo_nonexistent.yml | 21 ++++++++ .../integration/become/sudo_nopassword.yml | 26 ++++++++++ .../integration/become/sudo_password.yml | 50 +++++++++++++++++++ .../integration/become/sudo_requiretty.yml | 37 ++++++++++++++ 8 files changed, 197 insertions(+), 10 deletions(-) create mode 100644 tests/ansible/integration/become/all.yml create mode 100644 tests/ansible/integration/become/sudo_flags_failure.yml create mode 100644 tests/ansible/integration/become/sudo_nonexistent.yml create mode 100644 tests/ansible/integration/become/sudo_nopassword.yml create mode 100644 tests/ansible/integration/become/sudo_password.yml create mode 100644 tests/ansible/integration/become/sudo_requiretty.yml diff --git a/ansible_mitogen/connection.py b/ansible_mitogen/connection.py index b1724186..a2f99b1f 100644 --- a/ansible_mitogen/connection.py +++ b/ansible_mitogen/connection.py @@ -137,7 +137,26 @@ class Connection(ansible.plugins.connection.ConnectionBase): def connected(self): return self.broker is not None - def _wrap_connect(self, kwargs): + def _on_connection_error(self, msg): + raise ansible.errors.AnsibleConnectionFailure(msg) + + def _on_become_error(self, msg): + # TODO: vanilla become failures yield this: + # { + # "changed": false, + # "module_stderr": "sudo: sorry, you must have a tty to run sudo\n", + # "module_stdout": "", + # "msg": "MODULE FAILURE", + # "rc": 1 + # } + # + # Currently we yield this: + # { + # "msg": "EOF on stream; last 300 bytes received: 'sudo: ....\n'" + # } + raise ansible.errors.AnsibleModuleError(msg) + + def _wrap_connect(self, on_error, kwargs): dct = mitogen.service.call( context=self.parent, handle=ContextService.handle, @@ -146,7 +165,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): ) if dct['msg']: - raise ansible.errors.AnsibleConnectionFailure(dct['msg']) + on_error(dct['msg']) return dct['context'], dct['home_dir'] @@ -155,7 +174,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): Fetch a reference to the local() Context from ContextService in the master process. """ - return self._wrap_connect({ + return self._wrap_connect(self._on_connection_error, { 'method_name': 'local', 'python_path': self.python_path, }) @@ -165,7 +184,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): Fetch a reference to an SSH Context matching the play context from ContextService in the master process. """ - return self._wrap_connect({ + return self._wrap_connect(self._on_connection_error, { 'method_name': 'ssh', 'check_host_keys': False, # TODO 'hostname': self._play_context.remote_addr, @@ -189,7 +208,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): }) def _connect_docker(self): - return self._wrap_connect({ + return self._wrap_connect(self._on_connection_error, { 'method_name': 'docker', 'container': self._play_context.remote_addr, 'python_path': self.python_path, @@ -205,7 +224,7 @@ class Connection(ansible.plugins.connection.ConnectionBase): Parent Context of the sudo Context. For Ansible, this should always be a Context returned by _connect_ssh(). """ - return self._wrap_connect({ + return self._wrap_connect(self._on_become_error, { 'method_name': 'sudo', 'username': self._play_context.become_user, 'password': self._play_context.become_pass, @@ -213,10 +232,14 @@ class Connection(ansible.plugins.connection.ConnectionBase): 'sudo_path': self.sudo_path, 'connect_timeout': self._play_context.timeout, 'via': via, - 'sudo_args': shlex.split( - self._play_context.sudo_flags or - self._play_context.become_flags or '' - ), + 'sudo_args': [ + term + for s in ( + self._play_context.sudo_flags, + self._play_context.become_flags + ) + for term in shlex.split(s or '') + ], }) def _connect(self): diff --git a/tests/ansible/integration/all.yml b/tests/ansible/integration/all.yml index c9bb1908..352cd630 100644 --- a/tests/ansible/integration/all.yml +++ b/tests/ansible/integration/all.yml @@ -4,6 +4,7 @@ # - import_playbook: action/all.yml +- import_playbook: become/all.yml - import_playbook: connection_loader/all.yml - import_playbook: runner/all.yml - import_playbook: playbook_semantics/all.yml diff --git a/tests/ansible/integration/become/all.yml b/tests/ansible/integration/become/all.yml new file mode 100644 index 00000000..3c33ee75 --- /dev/null +++ b/tests/ansible/integration/become/all.yml @@ -0,0 +1,6 @@ + +- import_playbook: sudo_flags_failure.yml +- import_playbook: sudo_nonexistent.yml +- import_playbook: sudo_nopassword.yml +- import_playbook: sudo_password.yml +- import_playbook: sudo_requiretty.yml diff --git a/tests/ansible/integration/become/sudo_flags_failure.yml b/tests/ansible/integration/become/sudo_flags_failure.yml new file mode 100644 index 00000000..b0799846 --- /dev/null +++ b/tests/ansible/integration/become/sudo_flags_failure.yml @@ -0,0 +1,23 @@ +- hosts: all + any_errors_fatal: true + tasks: + - name: integration/become/sudo_flags_failure.yml + assert: + that: true + + - name: Verify behaviour for bad sudo flags. + shell: whoami + become: true + ignore_errors: true + register: out + vars: + ansible_become_flags: --derps + + - debug: msg={{out}} + - name: Verify raw module output. + assert: + that: + - out.failed + - | + ('sudo: no such option: --derps' in out.msg) or + ("sudo: unrecognized option `--derps'" in out.module_stderr) diff --git a/tests/ansible/integration/become/sudo_nonexistent.yml b/tests/ansible/integration/become/sudo_nonexistent.yml new file mode 100644 index 00000000..affcebfe --- /dev/null +++ b/tests/ansible/integration/become/sudo_nonexistent.yml @@ -0,0 +1,21 @@ +- hosts: all + any_errors_fatal: true + tasks: + - name: integration/become/sudo_nonexistent.yml + assert: + that: true + + - name: Verify behaviour for non-existent accounts. + shell: whoami + become: true + become_user: slartibartfast + ignore_errors: true + register: out + + - name: Verify raw module output. + assert: + that: | + out.failed and ( + ('sudo: unknown user: slartibartfast' in out.msg) or + ('sudo: unknown user: slartibartfast' in out.module_stderr) + ) diff --git a/tests/ansible/integration/become/sudo_nopassword.yml b/tests/ansible/integration/become/sudo_nopassword.yml new file mode 100644 index 00000000..5aa345d0 --- /dev/null +++ b/tests/ansible/integration/become/sudo_nopassword.yml @@ -0,0 +1,26 @@ +# Verify passwordless sudo behaviour in various cases. + +- hosts: all + any_errors_fatal: true + tasks: + - name: integration/become/sudo_basic.yml + assert: + that: true + + - name: Verify we aren't root + shell: whoami + register: out + + - assert: + that: + - out.stdout != 'root' + + - name: Ensure passwordless sudo to root succeeds. + shell: whoami + become: true + become_user: root + register: out + + - assert: + that: + - out.stdout == 'root' diff --git a/tests/ansible/integration/become/sudo_password.yml b/tests/ansible/integration/become/sudo_password.yml new file mode 100644 index 00000000..a399b59e --- /dev/null +++ b/tests/ansible/integration/become/sudo_password.yml @@ -0,0 +1,50 @@ +# Verify passwordful sudo behaviour + +- hosts: all + any_errors_fatal: true + tasks: + - name: integration/become/sudo_password.yml + assert: + that: true + + - name: Ensure password sudo absent. + shell: whoami + become: true + become_user: mitogen__pw_required + register: out + ignore_errors: true + + - assert: + that: | + out.failed and ( + ('password is required' in out.msg) or + ('password is required' in out.module_stderr) + ) + + - name: Ensure password sudo incorrect. + shell: whoami + become: true + become_user: mitogen__pw_required + register: out + vars: + ansible_become_pass: nopes + ignore_errors: true + + - assert: + that: | + out.failed and ( + ('Incorrect sudo password' in out.msg) or + ('sudo password is incorrect' in out.msg) + ) + + - name: Ensure password sudo succeeds. + shell: whoami + become: true + become_user: mitogen__pw_required + register: out + vars: + ansible_become_pass: mitogen__password + + - assert: + that: + - out.stdout == 'mitogen__pw_required' diff --git a/tests/ansible/integration/become/sudo_requiretty.yml b/tests/ansible/integration/become/sudo_requiretty.yml new file mode 100644 index 00000000..2af96c7e --- /dev/null +++ b/tests/ansible/integration/become/sudo_requiretty.yml @@ -0,0 +1,37 @@ +# Verify requiretty support + +- hosts: all + any_errors_fatal: true + tasks: + - name: integration/become/sudo_requiretty.yml + assert: + that: true + + - name: Verify we can login to a non-passworded requiretty account + shell: whoami + become: true + become_user: mitogen__require_tty + register: out + when: is_mitogen + + - assert: + that: + - out.stdout == 'mitogen__require_tty' + when: is_mitogen + + + # --------------- + + - name: Verify we can login to a passworded requiretty account + shell: whoami + become: true + become_user: mitogen__require_tty_pw_required + vars: + ansible_become_pass: mitogen__password + register: out + when: is_mitogen + + - assert: + that: + - out.stdout == 'mitogen__require_tty_pw_required' + when: is_mitogen