Commit ac98fe9e by Abhijit Menon-Sen Committed by James Cammarata

Implement ssh connection handling as a state machine

The event loop (even after it was brought into one place in _run in the
previous commit) was hard to follow. The states and transitions weren't
clear or documented, and the privilege escalation code was non-blocking
while the rest was blocking.

Now we have a state machine with four states: awaiting_prompt,
awaiting_escalation, ready_to_send (initial data), and awaiting_exit.
The actions in each state and the transitions between then are clearly
documented.

The check_incorrect_password() method no longer checks for empty strings
(since they will always match), and check_become_success() uses equality
rather than a substring match to avoid thinking an echoed command is an
indication of successful escalation. Also adds a check_missing_password
connection method to detect the error from sudo -n/doas -n.
parent 840a32bc
...@@ -172,6 +172,7 @@ DEFAULT_ASK_SUDO_PASS = get_config(p, DEFAULTS, 'ask_sudo_pass', 'ANSIBLE ...@@ -172,6 +172,7 @@ DEFAULT_ASK_SUDO_PASS = get_config(p, DEFAULTS, 'ask_sudo_pass', 'ANSIBLE
# Become # Become
BECOME_ERROR_STRINGS = {'sudo': 'Sorry, try again.', 'su': 'Authentication failure', 'pbrun': '', 'pfexec': '', 'runas': '', 'doas': 'Permission denied'} #FIXME: deal with i18n BECOME_ERROR_STRINGS = {'sudo': 'Sorry, try again.', 'su': 'Authentication failure', 'pbrun': '', 'pfexec': '', 'runas': '', 'doas': 'Permission denied'} #FIXME: deal with i18n
BECOME_MISSING_STRINGS = {'sudo': 'sorry, a password is required to run sudo', 'su': '', 'pbrun': '', 'pfexec': '', 'runas': '', 'doas': 'Authorization required'} #FIXME: deal with i18n
BECOME_METHODS = ['sudo','su','pbrun','pfexec','runas','doas'] BECOME_METHODS = ['sudo','su','pbrun','pfexec','runas','doas']
BECOME_ALLOW_SAME_USER = get_config(p, 'privilege_escalation', 'become_allow_same_user', 'ANSIBLE_BECOME_ALLOW_SAME_USER', False, boolean=True) BECOME_ALLOW_SAME_USER = get_config(p, 'privilege_escalation', 'become_allow_same_user', 'ANSIBLE_BECOME_ALLOW_SAME_USER', False, boolean=True)
DEFAULT_BECOME_METHOD = get_config(p, 'privilege_escalation', 'become_method', 'ANSIBLE_BECOME_METHOD','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo' ).lower() DEFAULT_BECOME_METHOD = get_config(p, 'privilege_escalation', 'become_method', 'ANSIBLE_BECOME_METHOD','sudo' if DEFAULT_SUDO else 'su' if DEFAULT_SU else 'sudo' ).lower()
......
...@@ -171,6 +171,9 @@ class PlayContext(Base): ...@@ -171,6 +171,9 @@ class PlayContext(Base):
self.password = passwords.get('conn_pass','') self.password = passwords.get('conn_pass','')
self.become_pass = passwords.get('become_pass','') self.become_pass = passwords.get('become_pass','')
self.prompt = ''
self.success_key = ''
# a file descriptor to be used during locking operations # a file descriptor to be used during locking operations
self.connection_lockfd = connection_lockfd self.connection_lockfd = connection_lockfd
......
...@@ -144,20 +144,23 @@ class ConnectionBase(with_metaclass(ABCMeta, object)): ...@@ -144,20 +144,23 @@ class ConnectionBase(with_metaclass(ABCMeta, object)):
pass pass
def check_become_success(self, output): def check_become_success(self, output):
return self._play_context.success_key in output return self._play_context.success_key == output.rstrip()
def check_password_prompt(self, output): def check_password_prompt(self, output):
if self._play_context.prompt is None: if self._play_context.prompt is None:
return False return False
elif isinstance(self._play_context.prompt, basestring): elif isinstance(self._play_context.prompt, basestring):
return output.endswith(self._play_context.prompt) return output.startswith(self._play_context.prompt)
else: else:
return self._play_context.prompt(output) return self._play_context.prompt(output)
def check_incorrect_password(self, output): def check_incorrect_password(self, output):
incorrect_password = gettext.dgettext(self._play_context.become_method, C.BECOME_ERROR_STRINGS[self._play_context.become_method]) incorrect_password = gettext.dgettext(self._play_context.become_method, C.BECOME_ERROR_STRINGS[self._play_context.become_method])
if incorrect_password in output: return incorrect_password and incorrect_password in output
raise AnsibleError('Incorrect %s password' % self._play_context.become_method)
def check_missing_password(self, output):
missing_password = gettext.dgettext(self._play_context.become_method, C.BECOME_MISSING_STRINGS[self._play_context.become_method])
return missing_password and missing_password in output
def connection_lock(self): def connection_lock(self):
f = self._play_context.connection_lockfd f = self._play_context.connection_lockfd
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment