def connect(self, host, port=PORT_NETCONF_DEFAULT, timeout=None, unknown_host_cb=default_unknown_host_cb, username=None, password=None, key_filename=None, allow_agent=True, hostkey_verify=True, hostkey_b64=None, look_for_keys=True, ssh_config=None, sock_fd=None): """Connect via SSH and initialize the NETCONF session. First attempts the publickey authentication method and then password authentication. To disable attempting publickey authentication altogether, call with *allow_agent* and *look_for_keys* as `False`. *host* is the hostname or IP address to connect to *port* is by default 830 (PORT_NETCONF_DEFAULT), but some devices use the default SSH port of 22 (PORT_SSH_DEFAULT) so this may need to be specified *timeout* is an optional timeout for socket connect *unknown_host_cb* is called when the server host key is not recognized. It takes two arguments, the hostname and the fingerprint (see the signature of :func:`default_unknown_host_cb`) *username* is the username to use for SSH authentication *password* is the password used if using password authentication, or the passphrase to use for unlocking keys that require it *key_filename* is a filename where a the private key to be used can be found *allow_agent* enables querying SSH agent (if found) for keys *hostkey_verify* enables hostkey verification from ~/.ssh/known_hosts *hostkey_b64* only connect when server presents a public hostkey matching this (obtain from server /etc/ssh/ssh_host_*pub or ssh-keyscan) *look_for_keys* enables looking in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`) *ssh_config* enables parsing of an OpenSSH configuration file, if set to its path, e.g. :file:`~/.ssh/config` or to True (in this case, use :file:`~/.ssh/config`). *sock_fd* is an already open socket which shall be used for this connection. Useful for NETCONF outbound ssh. Use host=None together with a valid sock_fd number """ if not (host or sock_fd): raise SSHError("Missing host or socket fd") self._host = host # Optionally, parse .ssh/config config = {} if ssh_config is True: ssh_config = "~/.ssh/config" if sys.platform != "win32" else "~/ssh/config" if ssh_config is not None: config = paramiko.SSHConfig() config.parse(open(os.path.expanduser(ssh_config))) # Save default Paramiko SSH port so it can be reverted paramiko_default_ssh_port = paramiko.config.SSH_PORT # Change the default SSH port to the port specified by the user so expand_variables # replaces %p with the passed in port rather than 22 (the defauld paramiko.config.SSH_PORT) paramiko.config.SSH_PORT = port config = config.lookup(host) # paramiko.config.SSHconfig::expand_variables is called by lookup so we can set the SSH port # back to the default paramiko.config.SSH_PORT = paramiko_default_ssh_port host = config.get("hostname", host) if username is None: username = config.get("user") if key_filename is None: key_filename = config.get("identityfile") if hostkey_verify: userknownhostsfile = config.get("userknownhostsfile") if userknownhostsfile: self.load_known_hosts( os.path.expanduser(userknownhostsfile)) if username is None: username = getpass.getuser() if sock_fd is None: if config.get("proxycommand"): self.logger.debug("Configuring Proxy. %s", config.get("proxycommand")) sock = paramiko.proxy.ProxyCommand(config.get("proxycommand")) else: for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: sock = socket.socket(af, socktype, proto) sock.settimeout(timeout) except socket.error: continue try: sock.connect(sa) except socket.error: sock.close() continue break else: raise SSHError("Could not open socket to %s:%s" % (host, port)) else: if sys.version_info[0] < 3: s = socket.fromfd(int(sock_fd), socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM, _sock=s) else: sock = socket.fromfd(int(sock_fd), socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(timeout) self._transport = paramiko.Transport(sock) self._transport.set_log_channel(logger.name) if config.get("compression") == 'yes': self._transport.use_compression() if hostkey_b64: # If we need to connect with a specific hostkey, negotiate for only its type hostkey_obj = None for key_cls in [ paramiko.DSSKey, paramiko.Ed25519Key, paramiko.RSAKey, paramiko.ECDSAKey ]: try: hostkey_obj = key_cls(data=base64.b64decode(hostkey_b64)) except paramiko.SSHException: # Not a key of this type - try the next pass if not hostkey_obj: # We've tried all known host key types and haven't found a suitable one to use - bail raise SSHError( "Couldn't find suitable paramiko key class for host key %s" % hostkey_b64) self._transport._preferred_keys = [hostkey_obj.get_name()] elif self._host_keys: # Else set preferred host keys to those we possess for the host # (avoids situation where known_hosts contains a valid key for the host, but that key type is not selected during negotiation) if port == PORT_SSH_DEFAULT: known_hosts_lookup = host else: known_hosts_lookup = '[%s]:%s' % (host, port) known_host_keys_for_this_host = self._host_keys.lookup( known_hosts_lookup) if known_host_keys_for_this_host: self._transport._preferred_keys = [ x.key.get_name() for x in known_host_keys_for_this_host._entries ] # Connect try: self._transport.start_client() except paramiko.SSHException as e: raise SSHError('Negotiation failed: %s' % e) server_key_obj = self._transport.get_remote_server_key() fingerprint = _colonify(hexlify(server_key_obj.get_fingerprint())) if hostkey_verify: is_known_host = False # For looking up entries for nonstandard (22) ssh ports in known_hosts # we enclose host in brackets and append port number if port == PORT_SSH_DEFAULT: known_hosts_lookup = host else: known_hosts_lookup = '[%s]:%s' % (host, port) if hostkey_b64: # If hostkey specified, remote host /must/ use that hostkey if (hostkey_obj.get_name() == server_key_obj.get_name() and hostkey_obj.asbytes() == server_key_obj.asbytes()): is_known_host = True else: # Check known_hosts is_known_host = self._host_keys.check(known_hosts_lookup, server_key_obj) if not is_known_host and not unknown_host_cb(host, fingerprint): raise SSHUnknownHostError(known_hosts_lookup, fingerprint) # Authenticating with our private key/identity if key_filename is None: key_filenames = [] elif isinstance(key_filename, (str, bytes)): key_filenames = [key_filename] else: key_filenames = key_filename self._auth(username, password, key_filenames, allow_agent, look_for_keys) self._connected = True # there was no error authenticating self._closing.clear() # TODO: leopoul: Review, test, and if needed rewrite this part subsystem_names = self._device_handler.get_ssh_subsystem_names() for subname in subsystem_names: self._channel = self._transport.open_session() self._channel_id = self._channel.get_id() channel_name = "%s-subsystem-%s" % (subname, str(self._channel_id)) self._channel.set_name(channel_name) try: self._channel.invoke_subsystem(subname) except paramiko.SSHException as e: self.logger.info("%s (subsystem request rejected)", e) handle_exception = self._device_handler.handle_connection_exceptions( self) # Ignore the exception, since we continue to try the different # subsystem names until we find one that can connect. # have to handle exception for each vendor here if not handle_exception: continue self._channel_name = self._channel.get_name() self._post_connect() return raise SSHError( "Could not open connection, possibly due to unacceptable" " SSH subsystem name.")
def test_do_connect_with_ssh_error(test_client, mock_manager): mock_manager.connect.side_effect = SSHError() with pytest.raises(SSHError): test_client._do_connect(10)
def connect(self, host, port=830, timeout=None, unknown_host_cb=default_unknown_host_cb, username=None, password=None, key_filename=None, allow_agent=True, hostkey_verify=True, look_for_keys=True, ssh_config=None): """Connect via SSH and initialize the NETCONF session. First attempts the publickey authentication method and then password authentication. To disable attempting publickey authentication altogether, call with *allow_agent* and *look_for_keys* as `False`. *host* is the hostname or IP address to connect to *port* is by default 830, but some devices use the default SSH port of 22 so this may need to be specified *timeout* is an optional timeout for socket connect *unknown_host_cb* is called when the server host key is not recognized. It takes two arguments, the hostname and the fingerprint (see the signature of :func:`default_unknown_host_cb`) *username* is the username to use for SSH authentication *password* is the password used if using password authentication, or the passphrase to use for unlocking keys that require it *key_filename* is a filename where a the private key to be used can be found *allow_agent* enables querying SSH agent (if found) for keys *hostkey_verify* enables hostkey verification from ~/.ssh/known_hosts *look_for_keys* enables looking in the usual locations for ssh keys (e.g. :file:`~/.ssh/id_*`) *ssh_config* enables parsing of an OpenSSH configuration file, if set to its path, e.g. :file:`~/.ssh/config` or to True (in this case, use :file:`~/.ssh/config`). """ # Optionaly, parse .ssh/config config = {} if ssh_config is True: ssh_config = "~/.ssh/config" if sys.platform != "win32" else "~/ssh/config" if ssh_config is not None: config = paramiko.SSHConfig() config.parse(open(os.path.expanduser(ssh_config))) config = config.lookup(host) host = config.get("hostname", host) if username is None: username = config.get("user") if key_filename is None: key_filename = config.get("identityfile") if username is None: username = getpass.getuser() sock = None if config.get("proxycommand"): sock = paramiko.proxy.ProxyCommand(config.get("proxycommand")) else: for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res try: sock = socket.socket(af, socktype, proto) sock.settimeout(timeout) except socket.error: continue try: sock.connect(sa) except socket.error: sock.close() continue break else: raise SSHError("Could not open socket to %s:%s" % (host, port)) t = self._transport = paramiko.Transport(sock) t.set_log_channel(logger.name) if config.get("compression") == 'yes': t.use_compression() try: t.start_client() except paramiko.SSHException: raise SSHError('Negotiation failed') # host key verification server_key = t.get_remote_server_key() fingerprint = _colonify(hexlify(server_key.get_fingerprint())) if hostkey_verify: known_host = self._host_keys.check(host, server_key) if not known_host and not unknown_host_cb(host, fingerprint): raise SSHUnknownHostError(host, fingerprint) if key_filename is None: key_filenames = [] elif isinstance(key_filename, (str, bytes)): key_filenames = [key_filename] else: key_filenames = key_filename self._auth(username, password, key_filenames, allow_agent, look_for_keys) self._connected = True # there was no error authenticating # TODO: leopoul: Review, test, and if needed rewrite this part subsystem_names = self._device_handler.get_ssh_subsystem_names() for subname in subsystem_names: c = self._channel = self._transport.open_session() self._channel_id = c.get_id() channel_name = "%s-subsystem-%s" % (subname, str(self._channel_id)) c.set_name(channel_name) try: c.invoke_subsystem(subname) except paramiko.SSHException as e: logger.info("%s (subsystem request rejected)", e) handle_exception = self._device_handler.handle_connection_exceptions( self) # Ignore the exception, since we continue to try the different # subsystem names until we find one that can connect. #have to handle exception for each vendor here if not handle_exception: continue self._channel_name = c.get_name() self._post_connect() return raise SSHError( "Could not open connection, possibly due to unacceptable" " SSH subsystem name.")