diff --git a/lib/ansible/module_utils/network.py b/lib/ansible/module_utils/network.py index b0b47bb26d..77a4ed66dc 100644 --- a/lib/ansible/module_utils/network.py +++ b/lib/ansible/module_utils/network.py @@ -32,7 +32,6 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import env_fallback, get_exception from ansible.module_utils.netcli import Cli, Command from ansible.module_utils.netcfg import Config -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO NET_TRANSPORT_ARGS = dict( host=dict(required=True), @@ -160,63 +159,6 @@ class NetworkModule(AnsibleModule): exc = get_exception() self.fail_json(msg=exc.message) -class NetCli(object): - """Basic paramiko-based ssh transport any NetworkModule can use.""" - - def __init__(self): - if not HAS_PARAMIKO: - raise NetworkError( - msg='paramiko is required but does not appear to be installed. ' - 'It can be installed using `pip install paramiko`' - ) - - self.shell = None - self._connected = False - self.default_output = 'text' - - def connect(self, params, kickstart=True, **kwargs): - host = params['host'] - port = params.get('port') or 22 - - username = params['username'] - password = params.get('password') - key_file = params.get('ssh_keyfile') - timeout = params['timeout'] - - try: - self.shell = Shell( - kickstart=kickstart, - prompts_re=self.CLI_PROMPTS_RE, - errors_re=self.CLI_ERRORS_RE, - ) - self.shell.open( - host, port=port, username=username, password=password, - key_filename=key_file, timeout=timeout, - ) - except ShellError: - exc = get_exception() - raise NetworkError( - msg='failed to connect to %s:%s' % (host, port), exc=str(exc) - ) - - self._connected = True - - def disconnect(self, **kwargs): - self.shell.close() - self._connected = False - - def authorize(self, params, **kwargs): - passwd = params['auth_pass'] - self.execute(Command('enable', prompt=self.NET_PASSWD_RE, response=passwd)) - - def execute(self, commands, **kwargs): - try: - return self.shell.send(commands) - except ShellError: - exc = get_exception() - raise NetworkError(exc.message, commands=commands) - - def register_transport(transport, default=False): def register(cls): NET_CONNECTIONS[transport] = cls diff --git a/lib/ansible/module_utils/shell.py b/lib/ansible/module_utils/shell.py index c944df2a66..33c1c093fa 100644 --- a/lib/ansible/module_utils/shell.py +++ b/lib/ansible/module_utils/shell.py @@ -18,8 +18,7 @@ # import re import socket - -from ansible.module_utils.basic import get_exception +import time # py2 vs py3; replace with six via ansiballz try: @@ -35,25 +34,11 @@ except ImportError: HAS_PARAMIKO = False from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkError -ANSI_RE = re.compile(r'(\x1b\[\?1h\x1b=)') - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") +ANSI_RE = [ + re.compile(r'(\x1b\[\?1h\x1b=)'), + re.compile(r'\x08.') ] def to_list(val): @@ -64,6 +49,7 @@ def to_list(val): else: return list() + class ShellError(Exception): def __init__(self, msg, command=None): @@ -71,15 +57,6 @@ class ShellError(Exception): self.message = msg self.command = command -class Command(object): - - def __init__(self, command, prompt=None, response=None): - self.command = command - self.prompt = prompt - self.response = response - - def __str__(self): - return self.command class Shell(object): @@ -90,8 +67,8 @@ class Shell(object): self.kickstart = kickstart self._matched_prompt = None - self.prompts = prompts_re or CLI_PROMPTS_RE - self.errors = errors_re or CLI_ERRORS_RE + self.prompts = prompts_re or list() + self.errors = errors_re or list() def open(self, host, port=22, username=None, password=None, timeout=10, key_filename=None, pkey=None, look_for_keys=None, @@ -123,10 +100,13 @@ class Shell(object): self.receive() def strip(self, data): - return ANSI_RE.sub('', data) + for regex in ANSI_RE: + data = regex.sub('', data) + return data def receive(self, cmd=None): recv = StringIO() + handled = False while True: data = self.shell.recv(200) @@ -136,12 +116,15 @@ class Shell(object): window = self.strip(recv.read()) - if isinstance(cmd, Command): - self.handle_input(window, prompt=cmd.prompt, - response=cmd.response) + if hasattr(cmd, 'prompt') and not handled: + if self.handle_prompt(window, prompt=cmd.prompt, response=cmd.response): + handled = True + time.sleep(cmd.delay) + if cmd.is_reboot: + return try: - if self.read(window): + if self.find_prompt(window): resp = self.strip(recv.getvalue()) return self.sanitize(cmd, resp) except ShellError: @@ -157,7 +140,7 @@ class Shell(object): self.shell.sendall(cmd) responses.append(self.receive(command)) except socket.timeout: - raise ShellError("timeout trying to send command", cmd) + raise ShellError("timeout trying to send command: %s" % cmd) except socket.error: exc = get_exception() raise ShellError("problem sending command to host: %s" % exc.message) @@ -166,7 +149,7 @@ class Shell(object): def close(self): self.shell.close() - def handle_input(self, resp, prompt, response): + def handle_prompt(self, resp, prompt, response): if not prompt or not response: return @@ -178,16 +161,17 @@ class Shell(object): if match: cmd = '%s\r' % ans self.shell.sendall(cmd) + return True def sanitize(self, cmd, resp): cleaned = [] for line in resp.splitlines(): - if line.startswith(str(cmd)) or self.read(line): + if line.startswith(str(cmd)) or self.find_prompt(line): continue cleaned.append(line) return "\n".join(cleaned) - def read(self, response): + def find_prompt(self, response): for regex in self.errors: if regex.search(response): raise ShellError('matched error in response: %s' % response) @@ -197,3 +181,60 @@ class Shell(object): if match: self._matched_prompt = match.group() return True + + +class CliBase(object): + """Basic paramiko-based ssh transport any NetworkModule can use.""" + + def __init__(self): + if not HAS_PARAMIKO: + raise NetworkError( + msg='paramiko is required but does not appear to be installed. ' + 'It can be installed using `pip install paramiko`' + ) + + self.shell = None + self._connected = False + self.default_output = 'text' + + def connect(self, params, kickstart=True, **kwargs): + host = params['host'] + port = params.get('port') or 22 + + username = params['username'] + password = params.get('password') + key_file = params.get('ssh_keyfile') + timeout = params['timeout'] + + try: + self.shell = Shell( + kickstart=kickstart, + prompts_re=self.CLI_PROMPTS_RE, + errors_re=self.CLI_ERRORS_RE, + ) + self.shell.open( + host, port=port, username=username, password=password, + key_filename=key_file, timeout=timeout, + ) + except ShellError: + exc = get_exception() + raise NetworkError( + msg='failed to connect to %s:%s' % (host, port), exc=str(exc) + ) + + self._connected = True + + def disconnect(self, **kwargs): + self.shell.close() + self._connected = False + + def authorize(self, params, **kwargs): + pass + + def execute(self, commands, **kwargs): + try: + return self.shell.send(commands) + except ShellError: + exc = get_exception() + raise NetworkError(exc.message, commands=commands) +