1# (c) 2016 Red Hat Inc.
2# (c) 2017 Ansible Project
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import absolute_import, division, print_function
6
7__metaclass__ = type
8
9DOCUMENTATION = """author: Ansible Networking Team
10connection: network_cli
11short_description: Use network_cli to run command on network appliances
12description:
13- This connection plugin provides a connection to remote devices over the SSH and
14  implements a CLI shell.  This connection plugin is typically used by network devices
15  for sending and receiving CLi commands to network devices.
16options:
17  host:
18    description:
19    - Specifies the remote device FQDN or IP address to establish the SSH connection
20      to.
21    default: inventory_hostname
22    vars:
23    - name: ansible_host
24  port:
25    type: int
26    description:
27    - Specifies the port on the remote device that listens for connections when establishing
28      the SSH connection.
29    default: 22
30    ini:
31    - section: defaults
32      key: remote_port
33    env:
34    - name: ANSIBLE_REMOTE_PORT
35    vars:
36    - name: ansible_port
37  network_os:
38    description:
39    - Configures the device platform network operating system.  This value is used
40      to load the correct terminal and cliconf plugins to communicate with the remote
41      device.
42    vars:
43    - name: ansible_network_os
44  remote_user:
45    description:
46    - The username used to authenticate to the remote device when the SSH connection
47      is first established.  If the remote_user is not specified, the connection will
48      use the username of the logged in user.
49    - Can be configured from the CLI via the C(--user) or C(-u) options.
50    ini:
51    - section: defaults
52      key: remote_user
53    env:
54    - name: ANSIBLE_REMOTE_USER
55    vars:
56    - name: ansible_user
57  password:
58    description:
59    - Configures the user password used to authenticate to the remote device when
60      first establishing the SSH connection.
61    vars:
62    - name: ansible_password
63    - name: ansible_ssh_pass
64    - name: ansible_ssh_password
65  private_key_file:
66    description:
67    - The private SSH key or certificate file used to authenticate to the remote device
68      when first establishing the SSH connection.
69    ini:
70    - section: defaults
71      key: private_key_file
72    env:
73    - name: ANSIBLE_PRIVATE_KEY_FILE
74    vars:
75    - name: ansible_private_key_file
76  become:
77    type: boolean
78    description:
79    - The become option will instruct the CLI session to attempt privilege escalation
80      on platforms that support it.  Normally this means transitioning from user mode
81      to C(enable) mode in the CLI session. If become is set to True and the remote
82      device does not support privilege escalation or the privilege has already been
83      elevated, then this option is silently ignored.
84    - Can be configured from the CLI via the C(--become) or C(-b) options.
85    default: false
86    ini:
87    - section: privilege_escalation
88      key: become
89    env:
90    - name: ANSIBLE_BECOME
91    vars:
92    - name: ansible_become
93  become_method:
94    description:
95    - This option allows the become method to be specified in for handling privilege
96      escalation.  Typically the become_method value is set to C(enable) but could
97      be defined as other values.
98    default: sudo
99    ini:
100    - section: privilege_escalation
101      key: become_method
102    env:
103    - name: ANSIBLE_BECOME_METHOD
104    vars:
105    - name: ansible_become_method
106  host_key_auto_add:
107    type: boolean
108    description:
109    - By default, Ansible will prompt the user before adding SSH keys to the known
110      hosts file.  Since persistent connections such as network_cli run in background
111      processes, the user will never be prompted.  By enabling this option, unknown
112      host keys will automatically be added to the known hosts file.
113    - Be sure to fully understand the security implications of enabling this option
114      on production systems as it could create a security vulnerability.
115    default: false
116    ini:
117    - section: paramiko_connection
118      key: host_key_auto_add
119    env:
120    - name: ANSIBLE_HOST_KEY_AUTO_ADD
121  persistent_connect_timeout:
122    type: int
123    description:
124    - Configures, in seconds, the amount of time to wait when trying to initially
125      establish a persistent connection.  If this value expires before the connection
126      to the remote device is completed, the connection will fail.
127    default: 30
128    ini:
129    - section: persistent_connection
130      key: connect_timeout
131    env:
132    - name: ANSIBLE_PERSISTENT_CONNECT_TIMEOUT
133    vars:
134    - name: ansible_connect_timeout
135  persistent_command_timeout:
136    type: int
137    description:
138    - Configures, in seconds, the amount of time to wait for a command to return from
139      the remote device.  If this timer is exceeded before the command returns, the
140      connection plugin will raise an exception and close.
141    default: 30
142    ini:
143    - section: persistent_connection
144      key: command_timeout
145    env:
146    - name: ANSIBLE_PERSISTENT_COMMAND_TIMEOUT
147    vars:
148    - name: ansible_command_timeout
149  persistent_buffer_read_timeout:
150    type: float
151    description:
152    - Configures, in seconds, the amount of time to wait for the data to be read from
153      Paramiko channel after the command prompt is matched. This timeout value ensures
154      that command prompt matched is correct and there is no more data left to be
155      received from remote host.
156    default: 0.1
157    ini:
158    - section: persistent_connection
159      key: buffer_read_timeout
160    env:
161    - name: ANSIBLE_PERSISTENT_BUFFER_READ_TIMEOUT
162    vars:
163    - name: ansible_buffer_read_timeout
164  persistent_log_messages:
165    type: boolean
166    description:
167    - This flag will enable logging the command executed and response received from
168      target device in the ansible log file. For this option to work 'log_path' ansible
169      configuration option is required to be set to a file path with write access.
170    - Be sure to fully understand the security implications of enabling this option
171      as it could create a security vulnerability by logging sensitive information
172      in log file.
173    default: false
174    ini:
175    - section: persistent_connection
176      key: log_messages
177    env:
178    - name: ANSIBLE_PERSISTENT_LOG_MESSAGES
179    vars:
180    - name: ansible_persistent_log_messages
181  terminal_stdout_re:
182    type: list
183    elements: dict
184    description:
185    - A single regex pattern or a sequence of patterns along with optional flags to
186      match the command prompt from the received response chunk. This option accepts
187      C(pattern) and C(flags) keys. The value of C(pattern) is a python regex pattern
188      to match the response and the value of C(flags) is the value accepted by I(flags)
189      argument of I(re.compile) python method to control the way regex is matched
190      with the response, for example I('re.I').
191    vars:
192    - name: ansible_terminal_stdout_re
193  terminal_stderr_re:
194    type: list
195    elements: dict
196    description:
197    - This option provides the regex pattern and optional flags to match the error
198      string from the received response chunk. This option accepts C(pattern) and
199      C(flags) keys. The value of C(pattern) is a python regex pattern to match the
200      response and the value of C(flags) is the value accepted by I(flags) argument
201      of I(re.compile) python method to control the way regex is matched with the
202      response, for example I('re.I').
203    vars:
204    - name: ansible_terminal_stderr_re
205  terminal_initial_prompt:
206    type: list
207    description:
208    - A single regex pattern or a sequence of patterns to evaluate the expected prompt
209      at the time of initial login to the remote host.
210    vars:
211    - name: ansible_terminal_initial_prompt
212  terminal_initial_answer:
213    type: list
214    description:
215    - The answer to reply with if the C(terminal_initial_prompt) is matched. The value
216      can be a single answer or a list of answers for multiple terminal_initial_prompt.
217      In case the login menu has multiple prompts the sequence of the prompt and excepted
218      answer should be in same order and the value of I(terminal_prompt_checkall)
219      should be set to I(True) if all the values in C(terminal_initial_prompt) are
220      expected to be matched and set to I(False) if any one login prompt is to be
221      matched.
222    vars:
223    - name: ansible_terminal_initial_answer
224  terminal_initial_prompt_checkall:
225    type: boolean
226    description:
227    - By default the value is set to I(False) and any one of the prompts mentioned
228      in C(terminal_initial_prompt) option is matched it won't check for other prompts.
229      When set to I(True) it will check for all the prompts mentioned in C(terminal_initial_prompt)
230      option in the given order and all the prompts should be received from remote
231      host if not it will result in timeout.
232    default: false
233    vars:
234    - name: ansible_terminal_initial_prompt_checkall
235  terminal_inital_prompt_newline:
236    type: boolean
237    description:
238    - This boolean flag, that when set to I(True) will send newline in the response
239      if any of values in I(terminal_initial_prompt) is matched.
240    default: true
241    vars:
242    - name: ansible_terminal_initial_prompt_newline
243  network_cli_retries:
244    description:
245    - Number of attempts to connect to remote host. The delay time between the retires
246      increases after every attempt by power of 2 in seconds till either the maximum
247      attempts are exhausted or any of the C(persistent_command_timeout) or C(persistent_connect_timeout)
248      timers are triggered.
249    default: 3
250    type: integer
251    env:
252    - name: ANSIBLE_NETWORK_CLI_RETRIES
253    ini:
254    - section: persistent_connection
255      key: network_cli_retries
256    vars:
257    - name: ansible_network_cli_retries
258"""
259
260from functools import wraps
261import getpass
262import json
263import logging
264import re
265import os
266import signal
267import socket
268import time
269import traceback
270from io import BytesIO
271
272from ansible.errors import AnsibleConnectionFailure
273from ansible.module_utils.six import PY3
274from ansible.module_utils.six.moves import cPickle
275from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
276    to_list,
277)
278from ansible.module_utils._text import to_bytes, to_text
279from ansible.playbook.play_context import PlayContext
280from ansible.plugins.connection import NetworkConnectionBase
281from ansible.plugins.loader import (
282    cliconf_loader,
283    terminal_loader,
284    connection_loader,
285)
286
287
288def ensure_connect(func):
289    @wraps(func)
290    def wrapped(self, *args, **kwargs):
291        if not self._connected:
292            self._connect()
293        self.update_cli_prompt_context()
294        return func(self, *args, **kwargs)
295
296    return wrapped
297
298
299class AnsibleCmdRespRecv(Exception):
300    pass
301
302
303class Connection(NetworkConnectionBase):
304    """ CLI (shell) SSH connections on Paramiko """
305
306    transport = "ansible.netcommon.network_cli"
307    has_pipelining = True
308
309    def __init__(self, play_context, new_stdin, *args, **kwargs):
310        super(Connection, self).__init__(
311            play_context, new_stdin, *args, **kwargs
312        )
313        self._ssh_shell = None
314
315        self._matched_prompt = None
316        self._matched_cmd_prompt = None
317        self._matched_pattern = None
318        self._last_response = None
319        self._history = list()
320        self._command_response = None
321        self._last_recv_window = None
322
323        self._terminal = None
324        self.cliconf = None
325        self._paramiko_conn = None
326
327        # Managing prompt context
328        self._check_prompt = False
329        self._task_uuid = to_text(kwargs.get("task_uuid", ""))
330
331        if self._play_context.verbosity > 3:
332            logging.getLogger("paramiko").setLevel(logging.DEBUG)
333
334        if self._network_os:
335            self._terminal = terminal_loader.get(self._network_os, self)
336            if not self._terminal:
337                raise AnsibleConnectionFailure(
338                    "network os %s is not supported" % self._network_os
339                )
340
341            self.cliconf = cliconf_loader.get(self._network_os, self)
342            if self.cliconf:
343                self._sub_plugin = {
344                    "type": "cliconf",
345                    "name": self.cliconf._load_name,
346                    "obj": self.cliconf,
347                }
348                self.queue_message(
349                    "vvvv",
350                    "loaded cliconf plugin %s from path %s for network_os %s"
351                    % (
352                        self.cliconf._load_name,
353                        self.cliconf._original_path,
354                        self._network_os,
355                    ),
356                )
357            else:
358                self.queue_message(
359                    "vvvv",
360                    "unable to load cliconf for network_os %s"
361                    % self._network_os,
362                )
363        else:
364            raise AnsibleConnectionFailure(
365                "Unable to automatically determine host network os. Please "
366                "manually configure ansible_network_os value for this host"
367            )
368        self.queue_message("log", "network_os is set to %s" % self._network_os)
369
370    @property
371    def paramiko_conn(self):
372        if self._paramiko_conn is None:
373            self._paramiko_conn = connection_loader.get(
374                "paramiko", self._play_context, "/dev/null"
375            )
376            self._paramiko_conn.set_options(
377                direct={
378                    "look_for_keys": not bool(
379                        self._play_context.password
380                        and not self._play_context.private_key_file
381                    )
382                }
383            )
384        return self._paramiko_conn
385
386    def _get_log_channel(self):
387        name = "p=%s u=%s | " % (os.getpid(), getpass.getuser())
388        name += "paramiko [%s]" % self._play_context.remote_addr
389        return name
390
391    @ensure_connect
392    def get_prompt(self):
393        """Returns the current prompt from the device"""
394        return self._matched_prompt
395
396    def exec_command(self, cmd, in_data=None, sudoable=True):
397        # this try..except block is just to handle the transition to supporting
398        # network_cli as a toplevel connection.  Once connection=local is gone,
399        # this block can be removed as well and all calls passed directly to
400        # the local connection
401        if self._ssh_shell:
402            try:
403                cmd = json.loads(to_text(cmd, errors="surrogate_or_strict"))
404                kwargs = {
405                    "command": to_bytes(
406                        cmd["command"], errors="surrogate_or_strict"
407                    )
408                }
409                for key in (
410                    "prompt",
411                    "answer",
412                    "sendonly",
413                    "newline",
414                    "prompt_retry_check",
415                ):
416                    if cmd.get(key) is True or cmd.get(key) is False:
417                        kwargs[key] = cmd[key]
418                    elif cmd.get(key) is not None:
419                        kwargs[key] = to_bytes(
420                            cmd[key], errors="surrogate_or_strict"
421                        )
422                return self.send(**kwargs)
423            except ValueError:
424                cmd = to_bytes(cmd, errors="surrogate_or_strict")
425                return self.send(command=cmd)
426
427        else:
428            return super(Connection, self).exec_command(cmd, in_data, sudoable)
429
430    def update_play_context(self, pc_data):
431        """Updates the play context information for the connection"""
432        pc_data = to_bytes(pc_data)
433        if PY3:
434            pc_data = cPickle.loads(pc_data, encoding="bytes")
435        else:
436            pc_data = cPickle.loads(pc_data)
437        play_context = PlayContext()
438        play_context.deserialize(pc_data)
439
440        self.queue_message("vvvv", "updating play_context for connection")
441        if self._play_context.become ^ play_context.become:
442            if play_context.become is True:
443                auth_pass = play_context.become_pass
444                self._terminal.on_become(passwd=auth_pass)
445                self.queue_message("vvvv", "authorizing connection")
446            else:
447                self._terminal.on_unbecome()
448                self.queue_message("vvvv", "deauthorizing connection")
449
450        self._play_context = play_context
451
452        if hasattr(self, "reset_history"):
453            self.reset_history()
454        if hasattr(self, "disable_response_logging"):
455            self.disable_response_logging()
456
457    def set_check_prompt(self, task_uuid):
458        self._check_prompt = task_uuid
459
460    def update_cli_prompt_context(self):
461        # set cli prompt context at the start of new task run only
462        if self._check_prompt and self._task_uuid != self._check_prompt:
463            self._task_uuid, self._check_prompt = self._check_prompt, False
464            self.set_cli_prompt_context()
465
466    def _connect(self):
467        """
468        Connects to the remote device and starts the terminal
469        """
470        if not self.connected:
471            self.paramiko_conn._set_log_channel(self._get_log_channel())
472            self.paramiko_conn.force_persistence = self.force_persistence
473
474            command_timeout = self.get_option("persistent_command_timeout")
475            max_pause = min(
476                [
477                    self.get_option("persistent_connect_timeout"),
478                    command_timeout,
479                ]
480            )
481            retries = self.get_option("network_cli_retries")
482            total_pause = 0
483
484            for attempt in range(retries + 1):
485                try:
486                    ssh = self.paramiko_conn._connect()
487                    break
488                except Exception as e:
489                    pause = 2 ** (attempt + 1)
490                    if attempt == retries or total_pause >= max_pause:
491                        raise AnsibleConnectionFailure(
492                            to_text(e, errors="surrogate_or_strict")
493                        )
494                    else:
495                        msg = (
496                            u"network_cli_retry: attempt: %d, caught exception(%s), "
497                            u"pausing for %d seconds"
498                            % (
499                                attempt + 1,
500                                to_text(e, errors="surrogate_or_strict"),
501                                pause,
502                            )
503                        )
504
505                        self.queue_message("vv", msg)
506                        time.sleep(pause)
507                        total_pause += pause
508                        continue
509
510            self.queue_message("vvvv", "ssh connection done, setting terminal")
511            self._connected = True
512
513            self._ssh_shell = ssh.ssh.invoke_shell()
514            self._ssh_shell.settimeout(command_timeout)
515
516            self.queue_message(
517                "vvvv",
518                "loaded terminal plugin for network_os %s" % self._network_os,
519            )
520
521            terminal_initial_prompt = (
522                self.get_option("terminal_initial_prompt")
523                or self._terminal.terminal_initial_prompt
524            )
525            terminal_initial_answer = (
526                self.get_option("terminal_initial_answer")
527                or self._terminal.terminal_initial_answer
528            )
529            newline = (
530                self.get_option("terminal_inital_prompt_newline")
531                or self._terminal.terminal_inital_prompt_newline
532            )
533            check_all = (
534                self.get_option("terminal_initial_prompt_checkall") or False
535            )
536
537            self.receive(
538                prompts=terminal_initial_prompt,
539                answer=terminal_initial_answer,
540                newline=newline,
541                check_all=check_all,
542            )
543
544            if self._play_context.become:
545                self.queue_message("vvvv", "firing event: on_become")
546                auth_pass = self._play_context.become_pass
547                self._terminal.on_become(passwd=auth_pass)
548
549            self.queue_message("vvvv", "firing event: on_open_shell()")
550            self._terminal.on_open_shell()
551
552            self.queue_message(
553                "vvvv", "ssh connection has completed successfully"
554            )
555
556        return self
557
558    def close(self):
559        """
560        Close the active connection to the device
561        """
562        # only close the connection if its connected.
563        if self._connected:
564            self.queue_message("debug", "closing ssh connection to device")
565            if self._ssh_shell:
566                self.queue_message("debug", "firing event: on_close_shell()")
567                self._terminal.on_close_shell()
568                self._ssh_shell.close()
569                self._ssh_shell = None
570                self.queue_message("debug", "cli session is now closed")
571
572                self.paramiko_conn.close()
573                self._paramiko_conn = None
574                self.queue_message(
575                    "debug", "ssh connection has been closed successfully"
576                )
577        super(Connection, self).close()
578
579    def receive(
580        self,
581        command=None,
582        prompts=None,
583        answer=None,
584        newline=True,
585        prompt_retry_check=False,
586        check_all=False,
587    ):
588        """
589        Handles receiving of output from command
590        """
591        self._matched_prompt = None
592        self._matched_cmd_prompt = None
593        recv = BytesIO()
594        handled = False
595        command_prompt_matched = False
596        matched_prompt_window = window_count = 0
597
598        # set terminal regex values for command prompt and errors in response
599        self._terminal_stderr_re = self._get_terminal_std_re(
600            "terminal_stderr_re"
601        )
602        self._terminal_stdout_re = self._get_terminal_std_re(
603            "terminal_stdout_re"
604        )
605
606        cache_socket_timeout = self._ssh_shell.gettimeout()
607        command_timeout = self.get_option("persistent_command_timeout")
608        self._validate_timeout_value(
609            command_timeout, "persistent_command_timeout"
610        )
611        if cache_socket_timeout != command_timeout:
612            self._ssh_shell.settimeout(command_timeout)
613
614        buffer_read_timeout = self.get_option("persistent_buffer_read_timeout")
615        self._validate_timeout_value(
616            buffer_read_timeout, "persistent_buffer_read_timeout"
617        )
618
619        self._log_messages("command: %s" % command)
620        while True:
621            if command_prompt_matched:
622                try:
623                    signal.signal(
624                        signal.SIGALRM, self._handle_buffer_read_timeout
625                    )
626                    signal.setitimer(signal.ITIMER_REAL, buffer_read_timeout)
627                    data = self._ssh_shell.recv(256)
628                    signal.alarm(0)
629                    self._log_messages(
630                        "response-%s: %s" % (window_count + 1, data)
631                    )
632                    # if data is still received on channel it indicates the prompt string
633                    # is wrongly matched in between response chunks, continue to read
634                    # remaining response.
635                    command_prompt_matched = False
636
637                    # restart command_timeout timer
638                    signal.signal(signal.SIGALRM, self._handle_command_timeout)
639                    signal.alarm(command_timeout)
640
641                except AnsibleCmdRespRecv:
642                    # reset socket timeout to global timeout
643                    self._ssh_shell.settimeout(cache_socket_timeout)
644                    return self._command_response
645            else:
646                data = self._ssh_shell.recv(256)
647                self._log_messages(
648                    "response-%s: %s" % (window_count + 1, data)
649                )
650            # when a channel stream is closed, received data will be empty
651            if not data:
652                break
653
654            recv.write(data)
655            offset = recv.tell() - 256 if recv.tell() > 256 else 0
656            recv.seek(offset)
657
658            window = self._strip(recv.read())
659            self._last_recv_window = window
660            window_count += 1
661
662            if prompts and not handled:
663                handled = self._handle_prompt(
664                    window, prompts, answer, newline, False, check_all
665                )
666                matched_prompt_window = window_count
667            elif (
668                prompts
669                and handled
670                and prompt_retry_check
671                and matched_prompt_window + 1 == window_count
672            ):
673                # check again even when handled, if same prompt repeats in next window
674                # (like in the case of a wrong enable password, etc) indicates
675                # value of answer is wrong, report this as error.
676                if self._handle_prompt(
677                    window,
678                    prompts,
679                    answer,
680                    newline,
681                    prompt_retry_check,
682                    check_all,
683                ):
684                    raise AnsibleConnectionFailure(
685                        "For matched prompt '%s', answer is not valid"
686                        % self._matched_cmd_prompt
687                    )
688
689            if self._find_prompt(window):
690                self._last_response = recv.getvalue()
691                resp = self._strip(self._last_response)
692                self._command_response = self._sanitize(resp, command)
693                if buffer_read_timeout == 0.0:
694                    # reset socket timeout to global timeout
695                    self._ssh_shell.settimeout(cache_socket_timeout)
696                    return self._command_response
697                else:
698                    command_prompt_matched = True
699
700    @ensure_connect
701    def send(
702        self,
703        command,
704        prompt=None,
705        answer=None,
706        newline=True,
707        sendonly=False,
708        prompt_retry_check=False,
709        check_all=False,
710    ):
711        """
712        Sends the command to the device in the opened shell
713        """
714        if check_all:
715            prompt_len = len(to_list(prompt))
716            answer_len = len(to_list(answer))
717            if prompt_len != answer_len:
718                raise AnsibleConnectionFailure(
719                    "Number of prompts (%s) is not same as that of answers (%s)"
720                    % (prompt_len, answer_len)
721                )
722        try:
723            cmd = b"%s\r" % command
724            self._history.append(cmd)
725            self._ssh_shell.sendall(cmd)
726            self._log_messages("send command: %s" % cmd)
727            if sendonly:
728                return
729            response = self.receive(
730                command, prompt, answer, newline, prompt_retry_check, check_all
731            )
732            return to_text(response, errors="surrogate_then_replace")
733        except (socket.timeout, AttributeError):
734            self.queue_message("error", traceback.format_exc())
735            raise AnsibleConnectionFailure(
736                "timeout value %s seconds reached while trying to send command: %s"
737                % (self._ssh_shell.gettimeout(), command.strip())
738            )
739
740    def _handle_buffer_read_timeout(self, signum, frame):
741        self.queue_message(
742            "vvvv",
743            "Response received, triggered 'persistent_buffer_read_timeout' timer of %s seconds"
744            % self.get_option("persistent_buffer_read_timeout"),
745        )
746        raise AnsibleCmdRespRecv()
747
748    def _handle_command_timeout(self, signum, frame):
749        msg = (
750            "command timeout triggered, timeout value is %s secs.\nSee the timeout setting options in the Network Debug and Troubleshooting Guide."
751            % self.get_option("persistent_command_timeout")
752        )
753        self.queue_message("log", msg)
754        raise AnsibleConnectionFailure(msg)
755
756    def _strip(self, data):
757        """
758        Removes ANSI codes from device response
759        """
760        for regex in self._terminal.ansi_re:
761            data = regex.sub(b"", data)
762        return data
763
764    def _handle_prompt(
765        self,
766        resp,
767        prompts,
768        answer,
769        newline,
770        prompt_retry_check=False,
771        check_all=False,
772    ):
773        """
774        Matches the command prompt and responds
775
776        :arg resp: Byte string containing the raw response from the remote
777        :arg prompts: Sequence of byte strings that we consider prompts for input
778        :arg answer: Sequence of Byte string to send back to the remote if we find a prompt.
779                A carriage return is automatically appended to this string.
780        :param prompt_retry_check: Bool value for trying to detect more prompts
781        :param check_all: Bool value to indicate if all the values in prompt sequence should be matched or any one of
782                          given prompt.
783        :returns: True if a prompt was found in ``resp``. If check_all is True
784                  will True only after all the prompt in the prompts list are matched. False otherwise.
785        """
786        single_prompt = False
787        if not isinstance(prompts, list):
788            prompts = [prompts]
789            single_prompt = True
790        if not isinstance(answer, list):
791            answer = [answer]
792        prompts_regex = [re.compile(to_bytes(r), re.I) for r in prompts]
793        for index, regex in enumerate(prompts_regex):
794            match = regex.search(resp)
795            if match:
796                self._matched_cmd_prompt = match.group()
797                self._log_messages(
798                    "matched command prompt: %s" % self._matched_cmd_prompt
799                )
800
801                # if prompt_retry_check is enabled to check if same prompt is
802                # repeated don't send answer again.
803                if not prompt_retry_check:
804                    prompt_answer = (
805                        answer[index] if len(answer) > index else answer[0]
806                    )
807                    self._ssh_shell.sendall(b"%s" % prompt_answer)
808                    if newline:
809                        self._ssh_shell.sendall(b"\r")
810                        prompt_answer += b"\r"
811                    self._log_messages(
812                        "matched command prompt answer: %s" % prompt_answer
813                    )
814                if check_all and prompts and not single_prompt:
815                    prompts.pop(0)
816                    answer.pop(0)
817                    return False
818                return True
819        return False
820
821    def _sanitize(self, resp, command=None):
822        """
823        Removes elements from the response before returning to the caller
824        """
825        cleaned = []
826        for line in resp.splitlines():
827            if command and line.strip() == command.strip():
828                continue
829
830            for prompt in self._matched_prompt.strip().splitlines():
831                if prompt.strip() in line:
832                    break
833            else:
834                cleaned.append(line)
835        return b"\n".join(cleaned).strip()
836
837    def _find_prompt(self, response):
838        """Searches the buffered response for a matching command prompt
839        """
840        errored_response = None
841        is_error_message = False
842
843        for regex in self._terminal_stderr_re:
844            if regex.search(response):
845                is_error_message = True
846
847                # Check if error response ends with command prompt if not
848                # receive it buffered prompt
849                for regex in self._terminal_stdout_re:
850                    match = regex.search(response)
851                    if match:
852                        errored_response = response
853                        self._matched_pattern = regex.pattern
854                        self._matched_prompt = match.group()
855                        self._log_messages(
856                            "matched error regex '%s' from response '%s'"
857                            % (self._matched_pattern, errored_response)
858                        )
859                        break
860
861        if not is_error_message:
862            for regex in self._terminal_stdout_re:
863                match = regex.search(response)
864                if match:
865                    self._matched_pattern = regex.pattern
866                    self._matched_prompt = match.group()
867                    self._log_messages(
868                        "matched cli prompt '%s' with regex '%s' from response '%s'"
869                        % (
870                            self._matched_prompt,
871                            self._matched_pattern,
872                            response,
873                        )
874                    )
875                    if not errored_response:
876                        return True
877
878        if errored_response:
879            raise AnsibleConnectionFailure(errored_response)
880
881        return False
882
883    def _validate_timeout_value(self, timeout, timer_name):
884        if timeout < 0:
885            raise AnsibleConnectionFailure(
886                "'%s' timer value '%s' is invalid, value should be greater than or equal to zero."
887                % (timer_name, timeout)
888            )
889
890    def transport_test(self, connect_timeout):
891        """This method enables wait_for_connection to work.
892
893        As it is used by wait_for_connection, it is called by that module's action plugin,
894        which is on the controller process, which means that nothing done on this instance
895        should impact the actual persistent connection... this check is for informational
896        purposes only and should be properly cleaned up.
897        """
898
899        # Force a fresh connect if for some reason we have connected before.
900        self.close()
901        self._connect()
902        self.close()
903
904    def _get_terminal_std_re(self, option):
905        terminal_std_option = self.get_option(option)
906        terminal_std_re = []
907
908        if terminal_std_option:
909            for item in terminal_std_option:
910                if "pattern" not in item:
911                    raise AnsibleConnectionFailure(
912                        "'pattern' is a required key for option '%s',"
913                        " received option value is %s" % (option, item)
914                    )
915                pattern = br"%s" % to_bytes(item["pattern"])
916                flag = item.get("flags", 0)
917                if flag:
918                    flag = getattr(re, flag.split(".")[1])
919                terminal_std_re.append(re.compile(pattern, flag))
920        else:
921            # To maintain backward compatibility
922            terminal_std_re = getattr(self._terminal, option)
923
924        return terminal_std_re
925