def _verify_connected(self): """ Helper function that verifies the connection has been established and that the transport object we are using is still connected. :raises ConnectionError: if we are not connected """ if not self.channel: raise exceptions.ConnectionError( context='Channel has not been started') if not self.sshprocess.is_connected(): raise exceptions.ConnectionError( context='Host SSH shell has been disconnected')
def open_interactive_channel(self, term='console', width=80, height=24): """ Creates and starts a stateful interactive channel. This should be used whenever the channel must remain open between commands for interactive processing, or when a terminal/tty is necessary; e.g., CLIs with modes. :param term: terminal type to emulate; defaults to 'console' :param width: width (in characters) of the terminal screen; defaults to 80 :param height: height (in characters) of the terminal screen; defaults to 24 :return: A Paramiko channel that communicate with the remote end in a stateful way. :raises ConnectionError: if the SSH connection has not yet been established. """ if (not self.is_connected()): raise exceptions.ConnectionError(context='Not connected!') channel = self.transport.open_session() channel.get_pty(term, width, height) channel.invoke_shell() channel.set_combine_stderr(True) return channel
def __init__(self, hostname, username, password=None, private_key_path=None, port=DEFAULT_PORT, terminal='console', width=DEFAULT_TERM_WIDTH, height=DEFAULT_TERM_HEIGHT, **kwargs): self.conn_port = port if password is None and private_key_path is None: cause = 'Either password or path to private key must be included.' raise exceptions.ConnectionError(cause=cause) pkey = None if private_key_path is not None: with open(private_key_path, 'r') as f: pkey = paramiko.rsakey.RSAKey.from_private_key(f) self.sshprocess = sshprocess.SSHProcess(host=hostname, user=username, password=password, private_key=pkey, port=self.conn_port) self._host = hostname self._term = terminal self._term_width = width self._term_height = height self.channel = None
def _verify_domain_running(self): """ Make sure domain is running. :raises ConnectionError: if it is not. """ info = self._domain.info() state = info[0] if state != libvirt.VIR_DOMAIN_RUNNING: raise exceptions.ConnectionError( context="Domain %s is not in running state" % self._machine_name)
def _reconnect(self, retry_count, retry_delay): if not isinstance(retry_count, int) or retry_count < 1: raise TypeError("retry_count should be positive int") if not isinstance(retry_delay, int) or retry_delay < 1: raise TypeError("retry_delay should be positive int") for count in range(retry_count): try: self.sshprocess.connect() return except exceptions.ConnectionError: logging.info("sleep %d second and retry..." % retry_delay) time.sleep(retry_delay) raise exceptions.ConnectionError("Failed to connect after " "%d retries" % retry_count)
def _verify_connected(self): """ Verifies an established connection and transport. :raises ConnectionError: if we are not connected """ if not self.channel: raise exceptions.ConnectionError( context='Channel has not been started') # Send an NOP to see whether connection is still alive. try: self.channel.sock.sendall(telnetlib.IAC + telnetlib.NOP) except socket.error: logging.exception('Host SSH shell has been disconnected') raise exceptions.ConnectionError
def send(self, text_to_send): """ Sends text to the channel immediately. Does not wait for any response. :param text_to_send: Text to send, may be an empty string. """ self._verify_connected() logging.debug('Sending "%s"' % self.safe_line_feeds(text_to_send)) bytes_sent = 0 while bytes_sent < len(text_to_send): bytes_sent_this_time = self.channel.send(text_to_send[bytes_sent:]) if bytes_sent_this_time == 0: raise exceptions.ConnectionError(context='Channel is closed') bytes_sent += bytes_sent_this_time
def start(self, match_res=(ROOT_PROMPT, ), timeout=DEFAULT_EXPECT_TIMEOUT): """ Opens a console and logs in. :param match_res: Pattern(s) of prompts to look for. May be a single regex string, or a list of them. :param timeout: maximum time, in seconds, to wait for a regular expression match. 0 to wait forever. :return: Python :class:`re.MatchObject` containing data on what was matched. """ if not HAS_LIBVIRT: raise ImportError("Failed to import libvirt") if not match_res: match_res = [self.ROOT_PROMPT] elif not (isinstance(match_res, list) or isinstance(match_res, tuple)): match_res = [ match_res, ] try: # Get connection and libvirt domain self._conn = libvirt.open(self._uri) self._domain = self._conn.lookupByName(self._machine_name) except libvirt.libvirtError: raise exceptions.ConnectionError( context="Failed to find domain '%s' on host" % self._machine_name) # Make sure domain is running self._verify_domain_running() # open console self._stream = self._conn.newStream(0) console_flags = libvirt.VIR_DOMAIN_CONSOLE_FORCE self._domain.openConsole(None, self._stream, console_flags) return self._handle_init_login(match_res, timeout)
def expect(self, match_res, timeout=DEFAULT_EXPECT_TIMEOUT): """ Waits for text to be received that matches one or more regex patterns. Note that data may have been received before this call and is waiting in the buffer; you may want to call receive_all() to flush the receive buffer before calling send() and call this function to match the output from your send() only. :param match_res: Pattern(s) to look for to be considered successful. May be a single regex string, or a list of them. Currently cannot match multiple lines. :param timeout: maximum time, in seconds, to wait for a regular expression match. 0 to wait forever. :return: ``(output, match_object)`` where output is the output of the command (without the matched text), and match_object is a Python :class:`re.MatchObject` containing data on what was matched. You may use ``MatchObject.string[m.start():m.end()]`` to recover the actual matched text, which will be unicode. ``re.MatchObject.pattern`` will contain the pattern that matched, which will be one of the elements of match_res passed in. :raises CmdlineTimeout: if no match found before timeout. :raises ConnectionError: if the channel is closed. """ match_res, safe_match_text = self._expect_init(match_res) received_data = '' # Index into received_data marking the start of the first unprocessed # line. next_line_start = 0 starttime = time.time() while True: # Use select to check whether channel is ready for read. # Reading on the channel directly would block until data is # ready, where select blocks at most 10 seconds which allows # us to check whether the specified timeout has been reached. # If channel is not ready for reading within 10 seconds, # select returns an empty list to 'readers'. (readers, w, x) = select.select([self.channel], [], [], 10) # Timeout if this is taking too long. if timeout and ((time.time() - starttime) > timeout): partial_output = repr(self.safe_line_feeds(received_data)) raise exceptions.CmdlineTimeout(command=None, output=partial_output, timeout=timeout, failed_match=match_res) new_data = None # We did not find clear documentation in Paramiko on how to check # whether a channel is closed unexpectedly. Our current logic is # that a channel is closed if: # (1) read the channel and get 0 bytes, or # (2) channel is not ready for reading but exit_status_ready() # Our experiments has shown that this correctly handles detecting # if a channel has been unexpected closed. if len(readers) > 0: new_data = self.channel.recv(4096) if len(new_data) == 0: # Channel closed raise exceptions.ConnectionError( failed_match=match_res, context='Channel unexpectedly closed') # If we're still here, we have new data to process. received_data, new_lines = self._process_data( new_data, received_data, next_line_start) output, match = self._match_lines(received_data, next_line_start, new_lines, match_res) if (output, match) != (None, None): return output, match # Update next_line_start to be the index of the last \n next_line_start = received_data.rfind('\n') + 1 elif self.channel.exit_status_ready(): raise exceptions.ConnectionError( failed_match=match_res, context='Channel unexpectedly closed')