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 __init__(self, url_data, destination=None): '''! Constructor for the URL class. @param url_data (dict) Information to be used to download the file @param destination (str, optional) When specified, this represents the filename used to save the \n downloaded file as. If destination value is available in url_data, this parameter is ignored. ''' argError = False self.__destination = None if isString(url_data): self.__source = url_data self.__destination = destination elif isinstance(url_data, dict) is False or \ url_data.get('source') is None or \ isString(url_data.get('source')) is False: argError = True elif isinstance(url_data, dict): self.__source = url_data.get('source') if url_data.get('destination') is None: self.__destination = destination else: self.__destination = url_data.get('destination') if self.__destination is not None and isString( self.__destination) is False: argError = True if argError: logger.debug('URL provided with invalid argument types.') raise TypeError('URL provided with invalid argument types.') self.url_data = url_data if isinstance(url_data, dict): self.objDownload = Downloader(self.__source, \ self.__destination, \ incl_http_headers=getField(self.url_data, 'include-http-headers', bool, None), \ is_secure=getField(self.url_data, 'secure', bool, None), \ curl_args=self.url_data.get('curl-arguments'), \ encrypted=getField(self.url_data, 'encrypted', bool, None), \ timeout=getField(self.url_data, 'timeout', int, None)) else: self.objDownload = Downloader(self.__source, self.__destination)
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 pluginArgs(self, section_name): '''! Resolve the plugin arguments used to be passed as command line arguments to the plugin used to process configuration section @param section_name (str) Configuration section name whose plugin arguments needs to be resolved. @return Concatenated string of all the argements defined \n If no arguments are defined or error encountered: \n None ''' if isString(section_name) is False: raise TypeError('Invalid argument used as section name') elif self.ztpDict.get(section_name) is None: logger.error('Configuration Section %s not found.' % section_name) return None # Obtain plugin data plugin_data = self.ztpDict.get(section_name).get('plugin') # Get the location of this configuration section's input data parsed from the input ZTP JSON file plugin_input = getCfg( 'ztp-tmp-persistent') + '/' + section_name + '/' + getCfg( 'section-input-file') plugin_args = '' ignore_section_data_arg = getField(plugin_data, 'ignore-section-data', bool, False) _plugin_json_args = getField(plugin_data, 'args', str, None) if ignore_section_data_arg is False: plugin_args = plugin_input if _plugin_json_args is not None: plugin_args = plugin_args + ' ' + _plugin_json_args logger.debug('Plugin arguments for %s evaluated to be %s.' % (section_name, plugin_args)) return plugin_args
def __buildDefaults(self): '''! Helper API to include missing objects in a configuration section and validate their values. ''' # Call more abstract API to insert default values self._ConfigSection__buildDefaults(self.ztpDict) default_objs = [ 'restart-ztp-on-failure', 'restart-ztp-no-config', 'config-fallback' ] # Loop through objects and update them with default values for key in default_objs: _val = getField(self.ztpDict, key, bool, getCfg(key)) self.ztpDict[key] = _val
def __init__(self, dyn_url_data, destination=None): '''! Constructor for the DynamicURL class. @param dyn_url_data (dict) Information to be used to download the file @param destination (str, optional) When specified, this represents the filename used to save the \n downloaded file as. If destination value is available in url_data, this parameter is ignored. @exception Raise ValueError if insufficient input is provided. @exception Raise TypeError if invalid input is provided or if switch identifier could not be resolved. Switch identifier string is resolved as part of the constructor. ''' if dyn_url_data is None or isinstance(dyn_url_data, dict) is False or \ dyn_url_data.get('source') is None or isinstance(dyn_url_data.get('source'), dict) is False or \ dyn_url_data.get('source').get('identifier') is None: logger.debug('DynamicURL provided with invalid argument types.') raise TypeError('DynamicURL provided with invalid argument types') self.__destination = None if dyn_url_data.get('destination') is not None: self.__destination = dyn_url_data.get('destination') elif destination is not None: self.__destination = destination if self.__destination is not None and \ isString(self.__destination) is False: logger.debug('DynamicURL provided with invalid argument types.') raise TypeError('DynamicURL provided with invalid argument types') self.dyn_url_data = dyn_url_data source = dyn_url_data.get('source') self.__source = '' if source.get('prefix') is not None: if isString(source.get('prefix')): self.__source = source.get('prefix') else: raise TypeError( 'DynamicURL provided with invalid argument types.') objIdentifier = Identifier(source.get('identifier')) identifier = objIdentifier.getIdentifier() if identifier is None: raise ValueError( 'DynamicURL source identifier could not be evaluated.') else: self.__source = self.__source + identifier if source.get('suffix') is not None: if isString(source.get('suffix')): self.__source = self.__source + source.get('suffix') else: raise TypeError( 'DynamicURL provided with invalid argument types.') self.objDownload = Downloader(self.__source, \ self.__destination, \ incl_http_headers=getField(self.dyn_url_data, 'include-http-headers', bool, None), \ is_secure=getField(self.dyn_url_data, 'secure', bool, None), \ curl_args=self.dyn_url_data.get('curl-arguments'), \ encrypted=getField(self.dyn_url_data, 'encrypted', bool, None), \ timeout=getField(self.dyn_url_data, 'timeout', int, None))
def test_getField(self): data = dict({'key': 'val'}) assert (getField(data, 'key', str, 'defval') == 'val') data = dict({'key': 10}) assert (getField(data, 'key', str, 'defval') == 'defval') data = dict({'key': 'val'}) assert (getField(data, 'key2', str, 'defval') == 'defval') data = dict({'key': True}) assert (getField(data, 'key', bool, False) == True) data = dict({'key': 'TrUe'}) assert (getField(data, 'key', bool, False) == True) data = dict({'key': 'fTrUe'}) assert (getField(data, 'key', bool, False) == False) data = dict({'key': 'FalSe'}) assert (getField(data, 'key2', bool, True) == True) data = dict({'key': 'FalSe'}) assert (getField(data, 'key', bool, True) == False) data = dict({'key': 10}) assert (getField(data, 'key', int, 20) == 10) data = dict({'key': "20"}) assert (getField(data, 'key', int, 10) == 20) data = dict({'key': "abc"}) assert (getField(data, 'key', int, 10) == 10) data = dict({'key': None}) assert (getField(data, 'key', int, 10) == 10) assert (getField(None, 'key', int, 10) == 10) assert (getField(None, None, int, 10) == 10) data = dict({'key': '10'}) assert (getField(data, 'key2', int, 20) == 20) data = dict({'key': '10'}) assert (getField(data, 'key', dict, None) == None) data = dict({'key': {'subkey': 10}}) assert (getField(data, 'key', dict, None).get('subkey') == 10)
def __processConfigSections(self): '''! Process and execute individual configuration sections defined in ZTP JSON. Plugin for each configuration section is resolved and executed. Configuration section data is provided as command line argument to the plugin. Each and every section is processed before this function returns. ''' # Obtain a copy of the list of configuration sections section_names = list(self.objztpJson.section_names) # set temporary flags abort = False sort = True logger.debug('Processing configuration sections: %s' % ', '.join(section_names)) # Loop through each sections till all of them are processed while section_names and abort is False: # Take a fresh sorted list to begin with and if any changes happen to it while processing if sort: sorted_list = sorted(section_names) sort = False # Loop through configuration section in a sorted order for sec in sorted_list: # Retrieve configuration section data section = self.objztpJson.ztpDict.get(sec) try: # Retrieve individual section's progress sec_status = section.get('status') if sec_status == 'BOOT' or sec_status == 'SUSPEND': # Mark section status as in progress self.objztpJson.updateStatus(section, 'IN-PROGRESS') if section.get('start-timestamp') is None: section['start-timestamp'] = section['timestamp'] self.objztpJson.objJson.writeJson() logger.info( 'Processing configuration section %s at %s.' % (sec, section['timestamp'])) elif sec_status != 'IN-PROGRESS': # Skip completed sections logger.debug( 'Removing section %s from list. Status %s.' % (sec, sec_status)) section_names.remove(sec) # set flag to sort the configuration sections list again sort = True # Ignore disabled configuration sections if sec_status == 'DISABLED': logger.info( 'Configuration section %s skipped as its status is set to DISABLED.' % sec) continue updateActivity('Processing configuration section %s' % sec) # Get the appropriate plugin to be used for this configuration section plugin = self.objztpJson.plugin(sec) # Get the location of this configuration section's input data parsed from the input ZTP JSON file plugin_input = getCfg( 'ztp-tmp-persistent') + '/' + sec + '/' + getCfg( 'section-input-file') # Initialize result flag to FAILED finalResult = 'FAILED' rc = 1 # Check if plugin could not be resolved if plugin is None: logger.error( 'Unable to resolve plugin to be used for configuration section %s. Marking it as FAILED.' % sec) section[ 'error'] = 'Unable to find or download requested plugin' elif os.path.isfile(plugin) and os.path.isfile( plugin_input): plugin_args = self.objztpJson.pluginArgs(sec) plugin_data = section.get('plugin') # Determine if shell has to be used to execute plugin _shell = getField(plugin_data, 'shell', bool, False) # Construct the full plugin command string along with arguments plugin_cmd = plugin if plugin_args is not None: plugin_cmd = plugin_cmd + ' ' + plugin_args # A plugin has been resolved and its input configuration section data as well logger.debug('Executing plugin %s.' % (plugin_cmd)) # Execute identified plugin rc = runCommand(plugin_cmd, capture_stdout=False, use_shell=_shell) logger.debug('Plugin %s exit code = %d.' % (plugin_cmd, rc)) # Compare plugin exit code if rc == 0: finalResult = 'SUCCESS' elif section.get('suspend-exit-code' ) is not None and section.get( 'suspend-exit-code') == rc: finalResult = 'SUSPEND' else: finalResult = 'FAILED' except Exception as e: logger.debug( 'Exception [%s] encountered for configuration section %s.' % (str(e), sec)) logger.info( 'Exception encountered while processing configuration section %s. Marking it as FAILED.' % sec) section[ 'error'] = 'Exception [%s] encountered while executing the plugin' % ( str(e)) finalResult = 'FAILED' # Update this configuration section's result in ztp json file logger.info( 'Processed Configuration section %s with result %s, exit code (%d) at %s.' % (sec, finalResult, rc, section['timestamp'])) if finalResult == 'FAILED' and section.get('error') is None: section['error'] = 'Plugin failed' section['exit-code'] = rc self.objztpJson.updateStatus(section, finalResult) # Check if abort ZTP on failure flag is set if getField(section, 'halt-on-failure', bool, False) is True and finalResult == 'FAILED': logger.info( 'Halting ZTP as Configuration section %s FAILED and halt-on-failure flag is set.' % sec) abort = True break # Check reboot on result flags self.__rebootAction(section)