def _RaiseExceptionIfLoadError(result, expect_config_check=False, expect_commit=False): """Checks if a result string from a load configuration contains an error. Args: result: A string, the result of loading the configuration. expect_config_check: A boolean. If true, then the exception-raising code will raise a special "configuration check failed" exception if the string "configuration check succeeds" isn't found. expect_commit: A boolean. If True, then the function raises an exception if the string "commit complete" isn't found on a line by itself. Raises: An exception derived from exceptions.SetConfigError if the result indicates an error, else nothing. """ # Remove output assumed to be part of diffs or quoted parts of the input. # Lines that are considered to be part of diffs start with + or - or !, # start and end with square brackets like "[edit ... ]", or immediately # follow a line that starts and ends with square brackets. lines = [] last_line_started_diff = False for line in result.splitlines(): if last_line_started_diff: last_line_started_diff = False # Ignore the line. elif line.startswith('[') and line.endswith(']'): last_line_started_diff = True else: lines.append(JunosDevice._CleanupErrorLine(line)) for error in JunosDevice.JUNOS_LOAD_ERRORS: if any(error in line for line in lines): break else: # No special "error" string found, check for "commit complete". if expect_commit and all('commit complete' not in line for line in lines): raise exceptions.SetConfigError( '"commit complete" expected, but not found in output:\n%s' % result) return # Raise the right type of exception based on the error string found. if any('syntax error' in line for line in lines): raise exceptions.SetConfigSyntaxError( 'Device reports a syntax error in the configuration.\n%s' % result) elif expect_config_check and all( 'configuration check succeeds' not in line for line in lines): raise exceptions.SetConfigSyntaxError( 'Configuration check failed.\n%s' % result) else: raise exceptions.SetConfigError( 'Error occurred during config load.\n%s' % result)
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)
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 SetConfig(self, destination_file, data, canary, juniper_skip_show_compare=False, juniper_skip_commit_check=False, juniper_get_rollback_patch=False): """Updates a devices' configuration. Concrete classes must define _SetConfig with the same arguments. Args: destination_file: A string. A path to a file on the device. data: A string, the configuration data to set. canary: A boolean, whether to canary, rather than set, the configuration. juniper_skip_show_compare: A boolean, temporary flag to skip 'show | compare' on Junipers due to a bug. juniper_skip_commit_check: A boolean, flag to skip 'commit check' on Junipers when doing a canary. juniper_get_rollback_patch: A boolean, optionally try to retrieve a patch to rollback the config change. Returns: A SetConfigResult. Transcript of any device interaction that occurred during the operation, plus any optional extras. Raises: exceptions.SetConfigError: the SetConfig operation failed. exceptions.SetConfigSyntaxError: the configuration data had a syntax error. """ if destination_file in self.unsupported_non_file_destinations: raise exceptions.SetConfigError( '%s devices do not support %s as a destination.' % (self.vendor_name, destination_file)) if ((juniper_skip_show_compare or juniper_skip_commit_check or juniper_get_rollback_patch) and self.__class__.__name__ == 'JunosDevice'): return self._SetConfig( destination_file, data, canary, skip_show_compare=juniper_skip_show_compare, skip_commit_check=juniper_skip_commit_check, get_rollback_patch=juniper_get_rollback_patch) else: return self._SetConfig(destination_file, data, canary)
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 _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
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)) elif destination_file == self.CONFIG_RUNNING: try: # This is not working, unfortunately. Cannot copy a file to a running # config, raised support case RFE2901 self._Cmd('copy slot1 running-config %s' % file_name) except exceptions.CmdError, e: raise exceptions.SetConfigError(str(e)) # We need to 'write memory' if we are doing running-config. logging.vlog( 3, 'Attempting to copy running-config to startup-config ' 'on %s(%s)', self.host, self.loopback_ipv4) try: self._Cmd('wr mem') except exceptions.CmdError, e: raise exceptions.SetConfigError( 'Failed to write startup-config ' 'for %s(%s). Error was: %s' % (self.host, self.loopback_ipv4, str(e))) finally: # Now remove the remote temporary file. # If this fails, we may have already copied the file, so log warnings
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