def Connect(self, username, password=None, ssh_keys=None, enable_password=None, ssl_cert_set=None): """Sets up a connection to the device. Concrete classes must implement _Connect() instead, with the same arguments. Concrete classes are expected not to disconnect the connection until it is cleaned-up by Disconnect(). A generic exception handler at the top- level should ensure sessions have an opportunity to be cleaned-up upon abnormal program termination. Args: username: A string, the username (role account) to use. password: A string, the password to use (optional; may be None). ssh_keys: A tuple of strings, SSH private keys (optional; may be None). enable_password: A string, an optional enable password (may be None). ssl_cert_set: An optional SSLCertificateSet protobuf (may be None). Raises: exceptions.ConnectError: the connection could not be established. exceptions.AuthenticationError: A device authentication error occurred, or neither a password nor an SSH private key was supplied. """ # Either an SSH key or password must be supplied for authentication. if (password is None and not ssh_keys and not ssl_cert_set and not FLAGS.use_ssh_agent): raise exceptions.AuthenticationError( 'Cannot connect. No authentication information provided to device ' 'Connect method.') self._username = username self._password = password self._ssh_keys = ssh_keys or () self._enable_password = enable_password self._ssl_cert_set = ssl_cert_set if not self.loopback_ipv4 and not self.accessproxy_device_dict: raise exceptions.ConnectError( 'Device %r, or any access proxies, need to have an IPv4 ' 'management address.' % self.host) logging.debug('In BaseDevice.Connect, host is %s, _connected is %s', self.host, self._connected) while not self.connected: try: if self._host_status: logging.debug('CONNECTING %s(%s)', self.host, self.loopback_ipv4) self._Connect(username, password=password, ssh_keys=self._ssh_keys, enable_password=enable_password, ssl_cert_set=ssl_cert_set) self.connected = True logging.debug('CONNECTED %s(%s)', self.host, self.loopback_ipv4) self._last_failure_time = None else: self._HostDownPrepareConnect() except (exceptions.ConnectError, exceptions.AuthenticationError), e: logging.error('CONNECT FAILURE %s(%s)', self.host, self.loopback_ipv4) self._host_status = False self.__exc = e raise
class BrocadeMlxDevice(BrocadeDevice): """A base device model suitable for Brocade MLX devices. See the base_device.BaseDevice method docstrings. """ disable_pager_command = _BROCADE_MLX_DISABLE_PAGER def __init__(self, **kwargs): self.vendor_name = 'brocademlx' super(BrocadeMlxDevice, self).__init__(**kwargs) def _GetFileSize(self, file_name, data): """Gets the size of a file in Brocade 'dir' output. Args: file_name: A string, the file name. data: A string, the Brocade's "dir" output. Returns: An int, the file size, or None if the value could not be determined. """ for line in data.splitlines(): match = RE_FILE_LISTING.match(line) if match is not None: (file_size, fname) = match.groups() for char in string.punctuation: file_size = file_size.replace(char, '') if file_name.strip() == fname.strip(): try: return int(file_size) except ValueError: continue return None def _SetConfig(self, destination_file, data, canary): # Canarying is not supported on BROCADE. if canary: raise exceptions.SetConfigCanaryingError( '%s devices do not support ' 'configuration canarying.' % self.vendor_name) # The result object. result = base_device.SetConfigResult() # Check for a connection to the Brocade. if not self._GetConnected(): raise exceptions.SetConfigError('Cannot use unless already ' 'connected to the device.') if destination_file in self.NON_FILE_DESTINATIONS: # Use a random remote file name file_name = 'push.%s' % os.urandom(8).encode('hex') else: # Okay, the user is just copying a file, not a configuraiton into either # startup-config or running-config, therefore we should use the entire # path. file_name = destination_file # Copy the file to the router using SCP. scp = pexpect_connection.ScpPutConnection(host=self.loopback_ipv4, username=self._username, password=self._password) # This is a workaround. Brocade case: 537017. # Brocade changed all the filename to lowercases after scp file_name = file_name.lower() try: scp.Copy(data, destination_file='slot1:' + file_name) except pexpect_connection.Error, e: raise exceptions.SetConfigError( 'Failed to copy configuration to remote device. %s' % str(e)) # Now that everything is OK locally and the file has been copied, # check the file and tell the device to set the new configuration. try: # Get the file size on the Brocade. try: cmd = 'dir /slot1/%s' % file_name dir_output = self._Cmd(cmd) except exceptions.CmdError, e: if 'Invalid input at' in str(e): raise exceptions.AuthenticationError( 'Username/password for %s(%s) has insufficient privileges ' 'to set configuration.' % (self.host, self.loopback_ipv4)) else: raise exceptions.SetConfigError( 'Could not traverse directory ' 'output. Command was: %r. ' 'Error: %r' % (cmd, str(e))) destination_file_size = self._GetFileSize(file_name, dir_output) # We couldn't parse the output for some reason. if destination_file_size is None: raise exceptions.SetConfigError( 'Could not find or parse remote ' 'file size after copy to device.') # Verify file is the correct size on the Brocade. # This should use a checksum (e.g. MD5 or SHA1); Brocade case: 609719. if destination_file_size != len(data): raise exceptions.SetConfigError( 'File transfer corrupted. Source file was: %d bytes, ' 'Destination file was: %d bytes.' % (len(data), destination_file_size)) # Copy the file from flash to the # destination(running-config, startup-config) if destination_file == self.CONFIG_STARTUP: try: self._connection.child.send( 'copy slot1 startup-config %s\r' % file_name) time.sleep(MINOR_PAUSE) pindex = self._connection.child.expect( ['Total bytes', self._connection.re_prompt, 'Error'], timeout=self.timeout_act_user) if pindex == 2: raise exceptions.SetConfigError( 'Could not copy temporary ' 'file to startup-config.') except (pexpect.EOF, pexpect.TIMEOUT), e: raise exceptions.SetConfigError(str(e))
def Connect(hostname, username, password=None, port=22, ssh_keys=(), timeout=TIMEOUT_DEFAULT): """Makes a paramiko SSH connection to a device. Args: hostname: A string, the hostname or IP address to connect to. username: A string, the username to use on the connection. password: A string, the password to use on the connection. port: An int, the port number to connect to. ssh_keys: A tuple of strings, SSH private keys (optional; may be None). timeout: A float, the number of seconds before a connection times out. Returns: A paramiko.SSHClient() instance """ options = SshOptions() hostname, port, username = options.Lookup(hostname, port, username) ssh_client = None def RaiseError(e, msg): """Raises an exception, disconnecting the SSH client. Args: e: An Exception. msg: An object, exception arguments. """ raise e(msg) try: ssh_client = paramiko.SSHClient() # Always auto-add remote SSH host keys. ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) ssh_client.load_system_host_keys() # Connect using paramiko with a timeout parameter (requires paramiko 1.7) if ssh_keys: pkeys = [] for key in ssh_keys: logging.debug( 'Using SSH private key for device authentication.') # Use a virtual temporary file to store the key. ssh_key_fileobj = cStringIO.StringIO() ssh_key_fileobj.write(key) ssh_key_fileobj.reset() try: pkeys.append(paramiko.DSSKey(file_obj=ssh_key_fileobj)) logging.debug('Using SSH DSA key for %r', hostname) except (IndexError, paramiko.SSHException) as e: if (isinstance(e, IndexError) or 'not a valid DSA private key file' in str(e)): ssh_key_fileobj.reset() try: logging.debug('Using SSH RSA key for %r', hostname) pkeys.append( paramiko.RSAKey(file_obj=ssh_key_fileobj)) except (IndexError, paramiko.SSHException) as e: raise exceptions.AuthenticationError(str(e)) else: raise exceptions.ConnectError('SSHException: %s' % str(e)) else: logging.debug('Using password for %r', hostname) pkeys = [None] for pkey in pkeys: saved_exception = None try: ssh_client.connect(hostname=hostname, port=port, username=username, password=password, pkey=pkey, timeout=timeout, allow_agent=FLAGS.use_ssh_agent, look_for_keys=False) break except (paramiko.AuthenticationException, paramiko.SSHException) as e: saved_exception = e if saved_exception is not None: raise saved_exception # pylint: disable=raising-bad-type transport = ssh_client.get_transport() # Sometimes we have to authenticate a second time, eg. on Force10 # we always fail the first authentication (if we try pkey + pass, # the pass succeeds; but if we do pass only, we have to do it # twice). connect() above will have authenticated once. if not transport.is_authenticated(): if pkeys != [None]: for pkey in pkeys: try: transport.auth_publickey(username, pkey) break except paramiko.SSHException: pass if not transport.is_authenticated(): if password is not None: try: transport.auth_password(username, password) except paramiko.SSHException: pass if not transport.is_authenticated(): msg = 'Not authenticated after two attempts on %r' % hostname RaiseError(exceptions.ConnectError, msg) except EOFError: msg = 'EOFError connecting to: %r' % hostname RaiseError(exceptions.ConnectError, msg) except paramiko.AuthenticationException as e: msg = 'Authentication error connecting to %s: %s' % (hostname, str(e)) RaiseError(exceptions.AuthenticationError, msg) except paramiko.SSHException as e: msg = 'SSHException connecting to %s: %s' % (hostname, str(e)) RaiseError(exceptions.ConnectError, msg) except socket.timeout as e: msg = 'Timed-out while connecting to %s: %s' % (hostname, str(e)) RaiseError(exceptions.ConnectError, msg) except socket.error as e: msg = 'Socket error connecting to %r: %s %s' % (hostname, e.__class__, e) RaiseError(exceptions.ConnectError, msg) return ssh_client