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))
def _JunosLoad(self, operation, filename, canary=False, skip_show_compare=False, skip_commit_check=False, rollback_patch=None): """Loads the configuration to the remote device using a given operation. Args: operation: A string, the load operation (e.g., 'replace', 'override'). filename: A string, the remote temporary filename to stage configuration. canary: A boolean, if True, only canary check the configuration, don't apply it. skip_show_compare: A boolean, if True, "show | compare" will be skipped. This is a temporary flag due to a JunOS bug and may be removed in the future. skip_commit_check: A boolean, if True, "commit check" (running the commit scripts) will be skipped in canary mode. rollback_patch: None or a string, optional filename into which to record and return a patch to rollback the config change. Returns: A base_device.SetConfigResult, all responses from the router during the check/load operation, plus any optional extras. """ show_compare = 'show | compare; ' if skip_show_compare: show_compare = '' if canary: commit_check = 'commit check; ' if skip_commit_check: commit_check = '' cmd = ('edit exclusive; load %s %s; %s%srollback 0; exit' % (operation, filename, show_compare, commit_check)) else: save_rollback_patch = '' if rollback_patch: save_rollback_patch = ( 'rollback 1; show | compare | save %s; rollback;' % rollback_patch) cmd = ('edit exclusive; load %s %s; %s' 'commit comment "push: load %s %s";%s exit' % (operation, filename, show_compare, operation, filename, save_rollback_patch)) result = base_device.SetConfigResult() result.transcript = self._Cmd(cmd) self._RaiseExceptionIfLoadError(result.transcript, expect_config_check=canary and not skip_commit_check, expect_commit=not canary) return result
def _SetConfig(self, unused_destination_file, data, canary): """Upload config to a Brocade router (TI/FI). Args: unused_destination_file: Unused. data: A string, the data to copy to destination_file. canary: A boolean, if True, only canary check the configuration, don't apply it. Returns: A base_device.SetConfigResult. Transcript of any device interaction that occurred during the _SetConfig. Raises: exceptions.CmdError: An error occurred inside the call to _Cmd. """ # 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.') # Derive our config prompt from the discovered prompt. self._connection.config_prompt = re.compile( self._connection.re_prompt.pattern[:-2] + r'\(config\S*\)' + self._connection.re_prompt.pattern[-2:]) # Enter config mode. self._connection.child.send('configure terminal\r') self._connection.child.expect('\r\n', timeout=self.timeout_response) self._connection.child.expect(self._connection.config_prompt, timeout=self.timeout_response, searchwindowsize=128) def SendAndWait(command): """Sends a command and waits for a response. Args: command: str; A single config line. Returns: A string; the last response. Raises: exceptions.SetConfigError: When we unexpectedly exit configuration mode while setting config. """ self._connection.child.send(command + '\r') self._connection.child.expect('\r\n', timeout=self.timeout_response) pindex = self._connection.child.expect( [self._connection.config_prompt, self._connection.re_prompt], timeout=self.timeout_response, searchwindowsize=128) # We unexpectedly exited config mode. Too many exits or ctrl-z. if pindex == 1: raise exceptions.SetConfigError( 'Unexpectedly exited config mode after line: %s' % command) return self._connection.child.before.replace('\r\n', os.linesep) lines = [x.strip() for x in data.splitlines()] # Remove any 'end' lines. Multiple ends could be bad. lines = [line for line in lines if line != 'end'] for line in lines: if next((line for prefix in self.verboten_config if line.startswith(prefix)), False): raise exceptions.CmdError( 'Command %s is not permitted on Brocade devices.' % line) if line: line_result = SendAndWait(line) if (line_result.startswith('Invalid input -> ') or line_result == 'Not authorized to execute this command.\n'): raise exceptions.CmdError('Command failed: %s' % line_result) self._connection.child.send('end\r') self._connection.child.expect(self._connection.re_prompt, timeout=self.timeout_act_user) self._connection.child.send('wr mem\r') self._connection.child.expect(self._connection.re_prompt, timeout=self.timeout_act_user) self._Disconnect() result.transcript = 'SetConfig applied the file successfully.' return result
def _SetConfig(self, destination_file, data, canary): # Canarying is not supported on ASA. if canary: raise exceptions.SetConfigCanaryingError('%s devices do not support ' 'configuration canarying.' % self.vendor_name) # We only support copying to 'running-config' or 'startup-config' on ASA. if destination_file not in ('running-config', 'startup-config'): raise exceptions.SetConfigError('destination_file argument must be ' '"running-config" or "startup-config" ' 'for %s devices.' % self.vendor_name) # Result object. result = base_device.SetConfigResult() # Get the MD5 sum of the file. local_digest = hashlib.md5(data).hexdigest() try: # Get the working path from the remote device remote_path = 'nvram:/' except exceptions.CmdError as e: msg = 'Error obtaining working directory: %s' % e logging.error(msg) raise exceptions.SetConfigError(msg) # Use a random remote file name remote_tmpfile = '%s/push.%s' % ( remote_path.rstrip(), os.urandom(8).encode('hex')) # Upload the file to the device. scp = pexpect_connection.ScpPutConnection( self.loopback_ipv4, username=self._username, password=self._password) try: scp.Copy(data, remote_tmpfile) except pexpect_connection.Error as e: raise exceptions.SetConfigError( 'Failed to copy configuration to remote device. %s' % str(e)) # Get the file size on the router. try: # Get the MD5 hexdigest of the file on the remote device. try: verify_output = self._Cmd('verify /md5 %s' % remote_tmpfile) match = MD5_RE.search(verify_output) if match is not None: remote_digest = match.group(1) else: raise exceptions.SetConfigError( 'The "verify /md5 <filename>" command did not produce ' 'expected results. It returned: %r' % verify_output) except exceptions.CmdError as e: raise exceptions.SetConfigError( 'The MD5 hash command on the router did not succed. ' 'The device may not support: "verify /md5 <filename>"') # Verify the local_digest and remote_digest are the same. if local_digest != remote_digest: raise exceptions.SetConfigError( 'File transfer to remote host corrupted. Local digest: %r, ' 'Remote digest: %r' % (local_digest, remote_digest)) # Copy the file from flash to the # destination(running-config, startup-config). # Catch errors that may occur during application, and report # these to the user. try: self._connection.child.send( 'copy %s %s\r' % (remote_tmpfile, destination_file)) pindex = self._connection.child.expect( [r'Destination filename \[%s\]\?' % destination_file, r'%\s*\S*.*', r'%Error.*', self._connection.re_prompt], timeout=self.timeout_act_user) if pindex == 0: self._connection.child.send('\r') try: pindex = self._connection.child.expect( [r'Invalid input detected', self._connection.re_prompt, r'%Warning:There is a file already existing.*' 'Do you want to over write\? \[confirm\]'], timeout=self.timeout_act_user) if pindex == 0: # Search again using findall to get all bad lines. bad_lines = re.findall( r'^(.*)$[\s\^]+% Invalid input', self._connection.child.match.string, re.MULTILINE) raise exceptions.SetConfigSyntaxError( 'Configuration loaded, but with bad lines:\n%s' % '\n'.join(bad_lines)) if pindex == 2: # Don't over-write. self._connection.child.send('n') raise exceptions.SetConfigError( 'Destination file %r already exists, cannot overwrite.' % destination_file) except (pexpect.EOF, pexpect.TIMEOUT) as e: raise exceptions.SetConfigError( 'Copied file to device, but did not ' 'receive prompt afterwards. %s %s' % (self._connection.child.before, self._connection.child.after)) elif pindex == 2: print "MATCHED 2" # The expect does a re.search, search again using findall to get all raise exceptions.SetConfigError('Could not copy temporary ' 'file to %s.' % destination_file) except (pexpect.EOF, pexpect.TIMEOUT) as e: raise exceptions.SetConfigError( 'Attempted to copy to bootflash, but a timeout occurred.') # We need to 'write memory' if we are doing running-config. if destination_file == 'running-config': logging.debug('Attempting to copy running-config to startup-config ' 'on %s(%s)', self.host, self.loopback_ipv4) try: self._Cmd('wr mem') except exceptions.CmdError as e: raise exceptions.SetConfigError('Failed to write startup-config ' 'for %s(%s). Changes applied. ' 'Error was: %s' % (self.host, self.loopback_ipv4, str(e))) finally: try: self._DeleteFile(remote_tmpfile) except DeleteFileError as e: result.transcript = 'SetConfig warning: %s' % str(e) logging.warn(result.transcript) # And finally, return the result text. return result
def _SetConfig(self, destination_file, data, canary, skip_show_compare=False, skip_commit_check=False, get_rollback_patch=False): copied = False file_ptr = tempfile.NamedTemporaryFile() rollback_patch_ptr = tempfile.NamedTemporaryFile() rollback_patch = None # Setting the file name based upon if we are trying to copy a file or # we are trying to copy a config into the control plane. if destination_file in self.NON_FILE_DESTINATIONS: file_name = os.path.basename(file_ptr.name) if get_rollback_patch: rollback_patch = os.path.basename(rollback_patch_ptr.name) else: file_name = destination_file logging.info('Remote file path: %s', file_name) try: file_ptr.write(data) file_ptr.flush() except IOError: raise exceptions.SetConfigError( 'Could not open temporary file %r' % file_ptr.name) result = base_device.SetConfigResult() try: # Copy the file to the remote device. try: self._SendFileViaSftp(local_filename=file_ptr.name, remote_filename=file_name) copied = True except (paramiko.SFTPError, IOError) as e: # _SendFileViaSftp puts the normalized destination path in e.args[1]. msg = 'SFTP failed (filename %r to device %s(%s):%s): %s: %s' % ( file_ptr.name, self.host, self.loopback_ipv4, e.args[1], e.__class__.__name__, e.args[0]) raise exceptions.SetConfigError(msg) if not self._ChecksumsMatch(local_file_name=file_ptr.name, remote_file_name=file_name): raise exceptions.SetConfigError( 'Local and remote file checksum mismatch.') if self.CONFIG_RUNNING == destination_file: operation = 'replace' elif self.CONFIG_STARTUP == destination_file: operation = 'override' elif self.CONFIG_PATCH == destination_file: operation = 'patch' else: result.transcript = 'SetConfig uploaded the file successfully.' return result if canary: logging.debug('Canary syntax checking configuration file %r.', file_name) result = self._JunosLoad(operation, file_name, canary=True, skip_show_compare=skip_show_compare, skip_commit_check=skip_commit_check) else: logging.debug( 'Setting destination %r with configuration file %r.', destination_file, file_name) result = self._JunosLoad(operation, file_name, skip_show_compare=skip_show_compare, skip_commit_check=skip_commit_check, rollback_patch=rollback_patch) if rollback_patch: try: self._GetFileViaSftp( local_filename=rollback_patch_ptr.name, remote_filename=rollback_patch) result.rollback_patch = rollback_patch_ptr.read() except (paramiko.SFTPError, IOError) as e: # _GetFileViaSftp puts the normalized source path in e.args[1]. result.transcript += ( 'SFTP rollback patch retrieval failed ' '(filename %r from device %s(%s):%s): %s: %s' % (rollback_patch_ptr.name, self.host, self.loopback_ipv4, e.args[1], e.__class__.__name__, e.args[0])) # Return the diagnostic results as the (optional) result. return result finally: local_delete_exception = None # Unlink the original temporary file. try: logging.info('Deleting the file on the local machine: %s', file_ptr.name) file_ptr.close() except IOError: local_delete_exception = exceptions.SetConfigError( 'Could not close temporary file.') local_rollback_patch_delete_exception = None # Unlink the rollback patch temporary file. try: logging.info('Deleting the file on the local machine: %s', rollback_patch_ptr.name) rollback_patch_ptr.close() except IOError: local_rollback_patch_delete_exception = exceptions.SetConfigError( 'Could not close temporary rollback patch file.') # If we copied the file to the router and we were pushing a configuration, # delete the temporary file off the router. if copied and destination_file in self.NON_FILE_DESTINATIONS: logging.info('Deleting file on the router: %s', file_name) self.Cmd('file delete ' + file_name) # Delete any rollback patch file too. if rollback_patch: logging.info('Deleting patch on the router: %s', rollback_patch) self.Cmd('file delete ' + rollback_patch) # If we got an exception on the local file delete, but did not get a # (more important) exception on the remote delete, raise the local delete # exception. # # pylint is confused by the re-raising # pylint: disable=raising-bad-type if local_delete_exception is not None: raise local_delete_exception if local_rollback_patch_delete_exception is not None: raise local_rollback_patch_delete_exception