def signal_handler(signum, frame): '''! This signal handler is called on SIGTERM or SIGINT ''' logger.warning('Received terminate signal. Shutting down.') updateActivity('Received terminate signal. Shutting down.') # Wait for some time count = getCfg('sighandler-wait-interval') while count > 0: done = True for pid in runcmd_pids: if check_pid(pid): done = False try: (wpid, status) = os.waitpid(pid, os.WNOHANG) if wpid == pid: print('Process pid %d returned with status %d.' % (pid, status)) except OSError as v: print('pid %d : %s' % (pid, str(v))) if done: break time.sleep(1) count -= 1 # Kill any process which might still be running for pid in runcmd_pids: if check_pid(pid): print('Process %d still alive, send kill signal.' % (pid)) os.kill(pid, signal.SIGKILL) sys.exit(0)
def __forceRestartDiscovery(self, msg): # Remove existing leases to source new provisioning data self.__cleanup_dhcp_leases() _msg = '%s. Waiting for %d seconds before restarting ZTP.' % ( msg, getCfg('restart-ztp-interval')) logger.warning(_msg) updateActivity(_msg) time.sleep(getCfg('restart-ztp-interval')) self.ztp_mode = 'DISCOVERY' # Force install of ZTP configuration profile self.__ztp_profile_loaded = False # Restart link-scan self.__intf_state = dict()
def __buildDefaults(self, section): '''! Helper API to include missing objects in a configuration section and validate their values. Below are the objects that are validated and added if not present: \n - ignore-result\n - reboot-on-success\n - reboot-on-failure\n - halt-on-failure\n - timestamp\n Below are the objects whose value is validated:\n - status - suspend-exit-code If the specified value is invalid of if the object is not specified, its value is read from ztp_cfg.json @param section (dict) Configuration Section input data read from JSON file. ''' default_objs = [ 'ignore-result', 'reboot-on-success', 'reboot-on-failure', 'halt-on-failure' ] # Loop through objects and update them with default values for key in default_objs: _val = getField(section, key, bool, getCfg(key)) section[key] = _val # set status if section.get('status') is None: section['status'] = 'BOOT' section['timestamp'] = getTimestamp() elif isString(section.get('status')) is False or \ section.get('status') not in ['BOOT', 'IN-PROGRESS', 'SUSPEND', 'DISABLED', 'FAILED', 'SUCCESS']: logger.warning( 'Invalid value (%s) used for configuration section status. Setting it to DISABLED.' % section.get('status')) section['status'] = 'DISABLED' section['timestamp'] = getTimestamp() # Validate suspend-exit code if section.get('suspend-exit-code') is not None: suspend_exit_code = getField(section, 'suspend-exit-code', int, None) if suspend_exit_code is None or suspend_exit_code < 0: del section['suspend-exit-code'] # Add timestamp if missing if section.get('timestamp') is None: section['timestamp'] = getTimestamp()
def __rebootAction(self, section, delayed_reboot=False): '''! Perform system reboot if reboot-on-success or reboot-on-failure is defined in the configuration section data. @param section (dict) Configuration section data containing status and reboot-on flags ''' # Obtain section status status = section.get('status') # Check if flag is set to reboot on SUCCESS and status is SUCCESS as well if getField(section, 'reboot-on-success', bool, False) is True and status == 'SUCCESS': logger.warning( 'ZTP is rebooting the device as reboot-on-success flag is set.' ) updateActivity('System reboot requested on success') if self.test_mode and delayed_reboot == False: sys.exit(0) else: if delayed_reboot: self.reboot_on_completion = True else: systemReboot() # Check if flag is set to reboot on FAIL and status is FAILED as well if getField(section, 'reboot-on-failure', bool, False) is True and status == 'FAILED': logger.warning( 'ZTP is rebooting the device as reboot-on-failure flag is set.' ) updateActivity('System reboot requested on failure') if self.test_mode and delayed_reboot == False: sys.exit(0) else: if delayed_reboot: self.reboot_on_completion = True else: systemReboot()
def executeLoop(self, test_mode=False): '''! ZTP service loop which peforms provisioning data discovery and initiates processing. ''' updateActivity('Initializing') # Set testing mode self.test_mode = test_mode # Check if ZTP is disabled administratively, bail out if disabled if getCfg('admin-mode') is False: logger.info('ZTP is administratively disabled.') self.__removeZTPProfile() return # Check if ZTP data restart flag is set if os.path.isfile(getCfg('ztp-restart-flag')): self.__ztp_restart = True os.remove(getCfg('ztp-restart-flag')) if self.test_mode: logger.warning( 'ZTP service started in test mode with restricted functionality.' ) else: logger.info('ZTP service started.') self.__ztp_engine_start_time = getTimestamp() _start_time = None self.ztp_mode = 'DISCOVERY' # Main provisioning data discovery loop while self.ztp_mode == 'DISCOVERY': updateActivity('Discovering provisioning data', overwrite=False) try: result = self.__discover() except Exception as e: logger.error( "Exception [%s] encountered while running the discovery logic." % (str(e))) _exc_type, _exc_value, _exc_traceback = sys.exc_info() __tb = traceback.extract_tb(_exc_traceback) for l in __tb: logger.debug(' File ' + l[0] + ', line ' + str(l[1]) + ', in ' + str(l[2])) logger.debug(' ' + str(l[3])) self.__forceRestartDiscovery( "Invalid provisioning data received") continue if result: if self.ztp_mode == 'MANUAL_CONFIG': logger.info( "Configuration file '%s' detected. Shutting down ZTP service." % (getCfg('config-db-json'))) break elif self.ztp_mode != 'DISCOVERY': (rv, msg) = self.__processZTPJson() if rv == "retry": self.ztp_mode = 'DISCOVERY' elif rv == "restart": self.__forceRestartDiscovery(msg) else: break # Initialize in-band interfaces to establish connectivity if not done already self.__loadZTPProfile("discovery") logger.debug('Provisioning data not found.') # Scan for inband interfaces to link up and restart interface connectivity if self.__link_scan(): updateActivity('Restarting network discovery after link scan') logger.info('Restarting network discovery after link scan.') runCommand('systemctl restart interfaces-config', capture_stdout=False) logger.info('Restarted network discovery after link scan.') _start_time = time.time() continue # Start keeping time of last time restart networking was done if _start_time is None: _start_time = time.time() # Check if we have to restart networking if (time.time() - _start_time > getCfg('restart-ztp-interval')): updateActivity('Restarting network discovery') if self.test_mode is False: # Remove existing leases to source new provisioning data self.__cleanup_dhcp_leases() logger.info('Restarting network discovery.') runCommand('systemctl restart interfaces-config', capture_stdout=False) logger.info('Restarted network discovery.') _start_time = time.time() continue # Try after sometime time.sleep(getCfg('discovery-interval')) # Cleanup installed ZTP configuration profile self.__removeZTPProfile() if self.reboot_on_completion and self.test_mode == False: updateActivity('System reboot requested') systemReboot() updateActivity('Exiting ZTP server')
def __processZTPJson(self): '''! Process ZTP JSON file downloaded using URL provided by DHCP Option 67, DHCPv6 Option 59 or local ZTP JSON file. ''' logger.debug('Starting to process ZTP JSON file %s.' % self.json_src) updateActivity('Processing ZTP JSON file %s' % self.json_src) try: # Read provided ZTP JSON file and load it self.objztpJson = ZTPJson(self.json_src, getCfg('ztp-json')) except ValueError as e: logger.error( 'Exception [%s] occured while processing ZTP JSON file %s.' % (str(e), self.json_src)) logger.error('ZTP JSON file %s processing failed.' % (self.json_src)) try: os.remove(getCfg('ztp-json')) if os.path.isfile(getCfg('ztp-json-shadow')): os.remove(getCfg('ztp-json-shadow')) except OSError as v: if v.errno != errno.ENOENT: logger.warning( 'Exception [%s] encountered while deleting ZTP JSON file %s.' % (str(v), getCfg('ztp-json'))) raise self.objztpJson = None # Restart networking after a wait time to discover new provisioning data if getCfg('restart-ztp-on-invalid-data'): return ("restart", "Invalid provisioning data processed") else: return ("stop", "Invalid provisioning data processed") if self.objztpJson['ztp-json-source'] is None: self.objztpJson['ztp-json-source'] = self.ztp_mode # Check if ZTP process has already completed. If not mark start of ZTP. if self.objztpJson['status'] == 'BOOT': self.objztpJson['status'] = 'IN-PROGRESS' if self.objztpJson['start-timestamp'] is None: self.objztpJson[ 'start-timestamp'] = self.__ztp_engine_start_time self.objztpJson.objJson.writeJson() elif self.objztpJson['status'] != 'IN-PROGRESS': # Re-start ZTP if requested if getCfg('monitor-startup-config') is True and self.__ztp_restart: self.__ztp_restart = False # Discover new ZTP data after deleting historic ZTP data logger.info( "ZTP restart requested. Deleting previous ZTP session JSON data." ) os.remove(getCfg('ztp-json')) if os.path.isfile(getCfg('ztp-json-shadow')): os.remove(getCfg('ztp-json-shadow')) self.objztpJson = None return ("retry", "ZTP restart requested") else: # ZTP was successfully completed in previous session. No need to proceed, return and exit service. logger.info( "ZTP already completed with result %s at %s." % (self.objztpJson['status'], self.objztpJson['timestamp'])) return ("stop", "ZTP completed") logger.info('Starting ZTP using JSON file %s at %s.' % (self.json_src, self.objztpJson['timestamp'])) # Initialize connectivity if not done already self.__loadZTPProfile("resume") # Process available configuration sections in ZTP JSON self.__processConfigSections() # Determine ZTP result self.__evalZTPResult() # Check restart ZTP condition # ZTP result is failed and restart-ztp-on-failure is set or _restart_ztp_on_failure = (self.objztpJson['status'] == 'FAILED' and \ self.objztpJson['restart-ztp-on-failure'] == True) # ZTP completed and no startup-config is found, restart-ztp-no-config and config-fallback is not set _restart_ztp_missing_config = ( (self.objztpJson['status'] == 'SUCCESS' or self.objztpJson['status'] == 'FAILED') and \ self.objztpJson['restart-ztp-no-config'] == True and \ self.objztpJson['config-fallback'] == False and os.path.isfile(getCfg('config-db-json')) is False ) # Mark ZTP for restart if _restart_ztp_missing_config or _restart_ztp_on_failure: os.remove(getCfg('ztp-json')) if os.path.isfile(getCfg('ztp-json-shadow')): os.remove(getCfg('ztp-json-shadow')) self.objztpJson = None # Remove startup-config file to obtain a new one through ZTP if getCfg('monitor-startup-config') is True and os.path.isfile( getCfg('config-db-json')): os.remove(getCfg('config-db-json')) if _restart_ztp_missing_config: return ( "restart", "ZTP completed but startup configuration '%s' not found" % (getCfg('config-db-json'))) elif _restart_ztp_on_failure: return ("restart", "ZTP completed with FAILED status") return ("stop", "ZTP completed")