PyEZ - Getting RpcTimeoutError after succesful commit even though timeout value is set to a high value

37 views Asked by At

I've got a Python (3.10) script using the PyEZ library to connect to Junos devices and commit various display set commands. Although both these steps are successful, when having committed the candidate configuration I get an RpcTimeoutError no matter what timeout value I set for the Device class and inside the commit() method of the Config class. I just don't get why this happens. The commit is done way before the 5 minutes are over and the commit_config() method should therefore return True.

The display set commands I commit:

delete interfaces ge-0/0/0 unit 500
delete class-of-service interfaces ge-0/0/0 unit 500
delete routing-options rib inet6.0 static route <ipv6 route>,

The error:

Error: RpcTimeoutError(host: hostname, cmd: commit-configuration, timeout: 360)

Relevant code is below:

DEVICE_TIMEOUT = 360 # RPC timeout value in seconds
DEVICE_AUTOPROBE = 15


class JunosDeviceConfigurator:
    def __init__(self, user=NETCONF_USER, password=NETCONF_PASSWD) -> None:
        self.user = user
        self.password = password
        self.device = None
        Device.auto_probe = DEVICE_AUTOPROBE
        Device.timeout = DEVICE_TIMEOUT

    def connect(self) -> bool:
        try:
            self.device = Device(
                host=self._hostname, 
                user=self.user, 
                passwd=self.password, 
                port=22, huge_tree=True, 
                gather_facts=True,
                timeout=DEVICE_TIMEOUT)
            self.device.open()
            self.device.timeout = DEVICE_TIMEOUT
            self.logger.info(f'Connected to {self._hostname}')
            return True
        except ConnectRefusedError as err:
            self.logger.error(f'Connection refused to {self._hostname}: {str(err)}')
            return False
        except ConnectError as err:
            self.logger.error(f'Connection to {self._hostname} failed: {str(err)}')
            return False
        except Exception as err:
            self.logger.error(f'Error connecting to {self._hostname}: {str(err)}')
            return False

    def commit_config(self, commands: list, mode = 'exclusive'):
        if not self.device:
            self.connect()

        try:
            with Config(self.device, mode=mode) as cu:
                for command in commands:
                    cu.load(command, format='set')
          
                cu.commit(timeout=DEVICE_TIMEOUT)
            
                return True
        except Exception as e:
             self.logger.error(f'Error: {str(e)}')
    
        return False
1

There are 1 answers

0
Beeelze On

I created a workaround to handle "fake" RPC timeouts. The method checks for a candidate configuration, if the output of cu.diff() is None, then the method returns True, otherwise there's a real RPC timeout and it will returns False after two retries. Here's the final code:

def commit_config(self, commands: list, mode='exclusive', max_retries=2) -> bool:
    """
    Commits configuration changes to a Juniper device using PyEZ.

    Args:
        commands (list): List of Junos OS configuration commands to be committed.
        mode (str, optional): The configuration mode to use ('exclusive' by default).
        max_retries (int, optional): Maximum number of retries in case of LockError or RpcTimeoutError.

    Returns:
        bool: True if the commit was successful, False otherwise.
    """
    if not self.device:
        self.connect()

    for _ in range(max_retries + 1):
        try:
            with Config(self.device, mode=mode) as cu:
                for command in commands:
                    cu.load(command, format='set')
                
                self.logger.info(f'Trying to commit candidate configuration on {self._hostname}.')
                cu.commit(timeout=DEVICE_TIMEOUT)
                
                return True
        except RpcTimeoutError as e:
            if cu.diff() is not None:
                self.logger.warning(f'RpcTimeoutError: {e}. Retrying in {RETRY_DELAY} seconds..')
                time.sleep(RETRY_DELAY)
            else:
                return True # Workaround: return True if the commit was successful despite RpcTimeoutError
        except LockError as e:
            self.logger.warning(f'LockError: {e}. Retrying in {RETRY_DELAY} seconds..')
            time.sleep(RETRY_DELAY)
        except ConfigLoadError as e:
            self.logger.warning(f'ConfigLoadError: {e}. Retrying in {RETRY_DELAY} seconds..')
            time.sleep(RETRY_DELAY)
        except CommitError as e:
            self.logger.error(e)
        except Exception as e:
            self.logger.error(f'Error: {str(e)}')
    
    return False

Any suggestions to improve this code are always welcome, but this "fix" will help me in the short run..