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