def setlogFile(self, log_file=None): '''! Set the log file to store all logs generated during ZTP @param log_file (str) log file @exception Raise TypeError if incorrect parameter type ''' rsyslog_conf_file = getCfg("rsyslog-ztp-log-file-conf", '/etc/rsyslog.d/10-ztp.conf') if log_file is None or (isString(log_file) and log_file == ''): if os.path.isfile(rsyslog_conf_file): os.remove(rsyslog_conf_file) return if not isString(log_file): raise TypeError("Log file must be a string") self.__log_file = log_file change = True if os.path.isfile(rsyslog_conf_file): fh = open(rsyslog_conf_file, 'r') if fh.readline().strip( ) == ':programname, contains, "sonic-ztp" ' + log_file: change = False fh.close() if change: fh = open(rsyslog_conf_file, 'w') fh.write(':programname, contains, "sonic-ztp" ' + log_file) fh.close() runCommand('systemctl restart rsyslog', capture_stdout=False)
def __cleanup_dhcp_leases(self): # Use ZTP interface used to obtain provisioning information runCommand('rm -f /var/lib/dhcp/dhclient*.eth0.leases', capture_stdout=False) if getCfg('feat-inband'): runCommand('rm -f /var/lib/dhcp/dhclient*.Ethernet*.leases', capture_stdout=False)
def test_json_config_valid_load(self, tmpdir): '''! Test case when the new json configuration file to apply is syntaxically valid. Verify that the new configuration is correctly applied using 'config load'. Verify that the plugin does not return with a non zero exit code. ''' d = tmpdir.mkdir("valid") fh_before = d.join("config-before.json") cmd = 'config save -y ' + str(fh_before) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0) data = self.__read_file(str(fh_before)) fh_after = d.join("config-after.json") self.__write_file(str(fh_after), data) cmd = "/bin/sed -i -e 's/\"hostname\": \".*\"/\"hostname\": \"something\"/' " + str( fh_after) rc = runCommand(cmd) fh = d.join("input.json") fh.write(""" { "configdb-json": { "clear-config": false, "halt-on-failure": false, "ignore-result": false, "reboot-on-failure": false, "reboot-on-success": false, "status": "BOOT", "timestamp": "2019-05-01 19:49:25", "url": { "destination": "/etc/sonic/config_db.json", "source": "file://%s" } } } """ % (str(fh_after))) configdb_json = ConfigDBJson(str(fh)) configdb_json.main() fh_after = d.join("config-after.json") cmd = 'config save -y ' + str(fh_after) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0) # Collect the differences between the two configurations cmd = "/usr/bin/diff --changed-group-format='%>' --unchanged-group-format='' " + str( fh_before) + ' ' + str(fh_after) (rc2, cmd_stdout, cmd_stderr) = runCommand(cmd) # Restore initial configuration cmd = 'config load -y ' + str(fh_before) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0)
def __link_scan(self): '''! Scan all in-band interface's operational status to detect a link up event ''' # Do not attempt link scan when in test mode if self.test_mode: return False if self.__link_scan_enabled is None: # Check if ZTP configuration is active (rc, op, errStr ) = runCommand("redis-cli -n 4 HGET \"ZTP|mode\" \"profile\"") if rc == 0 and len(op) != 0 and op[0] == 'active': self.__link_scan_enabled = 'True' else: self.__link_scan_enabled = 'False' if self.__link_scan_enabled == 'False': return False link_scan_result = False (rc, port_table_data, err) = runCommand('redis-dump -d 0 -k PORT_TABLE:Ethernet*') if rc != 0: port_table = None else: try: port_table = json.loads(''.join(port_table_data)) except: port_table = None intf_data = os.listdir('/sys/class/net') if getCfg('feat-inband'): r_intf = re.compile("Ethernet.*|eth.*") else: r_intf = re.compile("eth.*") intf_list = list(filter(r_intf.match, intf_data)) for intf in intf_list: try: if intf[0:3] == 'eth': fh = open('/sys/class/net/{}/operstate'.format(intf), 'r') operstate = fh.readline().strip().lower() fh.close() else: operstate = port_table.get('PORT_TABLE:' + intf).get( 'value').get('oper_status').lower() except: operstate = 'down' if ((self.__intf_state.get(intf) is None) or \ (self.__intf_state.get(intf) != operstate)) and \ operstate == 'up': link_scan_result = True logger.info('Link up detected for interface %s' % intf) self.__intf_state[intf] = operstate return link_scan_result
def test_runCommand(self): '''! Test the runCommand function ''' with pytest.raises(TypeError): (rc, cmd_stdout, cmd_stderr) = runCommand([12]) (rc, cmd_stdout, cmd_stderr) = runCommand('/abc/xyz/foo') assert ((rc, cmd_stdout, cmd_stderr) == (1, None, None)) (rc, cmd_stdout, cmd_stderr) = runCommand('ps FOO') assert (rc == 1) (rc, cmd_stdout, cmd_stderr) = runCommand('[12]')
def getIdentifier(self): '''! Obtain the resolved identifier string based on the identifier data. @return In case of success: \n Identifier string \n In case of error: \n None ''' try: identifier_data = self.identifier_data if isinstance(identifier_data, dict) is False: if identifier_data == "hostname": return socket.gethostname() elif identifier_data == "hostname-fqdn": return socket.getfqdn() elif identifier_data == "serial-number": return sysEeprom.get_serial_number() elif identifier_data == "product-name": return sysEeprom.get_product_name() elif identifier_data == "sonic-version": return get_sonic_version() else: return identifier_data elif identifier_data.get('url') is not None: url_data = identifier_data.get('url') (fd, filename) = tempfile.mkstemp(prefix='identifier_', dir=getCfg('ztp-tmp')) os.close(fd) urlObj = URL(url_data, filename) updateActivity('Downloading identifier script from \'%s\'' % (urlObj.getSource())) rc, identifier_script = urlObj.download() if rc == 0 and os.path.isfile(identifier_script): updateActivity( 'Executing identifier script downloaded from \'%s\'' % (urlObj.getSource())) (rc, cmd_stdout, cmd_stderr) = runCommand([identifier_script]) if rc != 0: logger.error( 'Error encountered while executing identifier %s Exit code: (%d).' % (identifier_script, rc)) return None if len(cmd_stdout) == 0: return "" else: return cmd_stdout[0] except (TypeError, ValueError) as e: logger.error( 'Exception: [%s] encountered while processing identifier data in dynamic URL.' % str(e)) return None return None
def __read_sys_eeprom(self, option): '''! Helper function to read a specific section from the eeprom. @param option (str) Option given to decode_syseeprom command line utility ''' cmd = 'decode-syseeprom ' + option (rc, cmd_stdout, cmd_stderr) = runCommand(cmd) if not rc == 0 or len(cmd_stdout) != 1: return 'N.A' else: return printable(cmd_stdout[0].rstrip())
def __removeZTPProfile(self): '''! If ZTP configuration profile is operational, remove ZTP configuration profile and load startup configuration file. If there is no startup configuration file, load factory default configuration. ''' # Do not attempt to remove ZTP configuration if working in unit test mode if self.test_mode: return # Remove ZTP configuration profile if loaded updateActivity('Verifying configuration') # Use a fallback default configuration if configured to _config_fallback = '' if (self.objztpJson is not None and (self.objztpJson['status'] == 'FAILED' or self.objztpJson['status'] == 'SUCCESS') \ and self.objztpJson['config-fallback']) or \ (self.objztpJson is None and getCfg('config-fallback') is True): _config_fallback = ' config-fallback' # Execute profile removal command with appropriate options rc = runCommand(getCfg('ztp-lib-dir') + '/ztp-profile.sh remove' + _config_fallback, capture_stdout=False) # Remove ZTP configuration startup-config if os.path.isfile(getCfg('config-db-json')) is True: try: config_db = None with open(getCfg('config-db-json')) as json_file: config_db = json.load(json_file) json_file.close() if config_db is not None and config_db.get('ZTP'): logger.info("Deleting ZTP configuration saved in '%s'." % (getCfg('config-db-json'))) del config_db['ZTP'] with open(getCfg('config-db-json'), 'w') as json_file: json.dump(config_db, json_file, indent=4) json_file.close() except Exception as e: logger.error( "Exception [%s] encountered while verifying '%s'." % (str(e), getCfg('config-db-json'))) self.__ztp_profile_loaded = False
def __loadZTPProfile(self, event): '''! Load ZTP configuration profile if there is no saved configuration file. This establishes connectivity to all interfaces and starts DHCP discovery. ''' # Do not attempt to install ZTP configuration if working in unit test mode if self.test_mode: return False if self.__ztp_profile_loaded is False: updateActivity('Checking running configuration') logger.info( 'Checking running configuration to load ZTP configuration profile.' ) cmd = getCfg('ztp-lib-dir') + '/ztp-profile.sh install ' + event # When performing ZTP discovery, force load ZTP profile. When # ZTP is resuming previous session, use configuration already loaded during # config-setup rc = runCommand(cmd, capture_stdout=False) self.__ztp_profile_loaded = True return True return False
def getUrl(self, url=None, dst_file=None, incl_http_headers=None, is_secure=True, timeout=None, retry=None, curl_args=None, encrypted=None, verbose=False): '''! Fetch a file using a given url. The content retrieved from the server is stored into a file. In case of a server erreur, the html content is stored into the file. This can be used to have a finer diagnostic of the issue. @param url (str, optional) url of the file you want to get. \n If the url is not given, the one specified in the constructor will be used. @param dst_file (str, optional) Filename for the data being stored. \n If not specified here and in the colnstructor, it will be derived from the url \n (last part of it, e.g. basename). @param incl_http_headers (bool, optional) Include or not the additional HTTP headers \n (product name, serial number, mac address) @param is_secure (bool, optional) Every SSL connection curl makes is verified or not to be secure. \n By default, the SSL connection is assumed secure. @param timeout (int, optional) Maximum number of seconds allowed for curl's connection to take. \n This only limits the connection phase. @param retry (int, optional) Number of times curl will retry in case of a transient error @param curl_args (str, optional) Options you want to pass to curl command line program. \n If no parameters are given here, the one given in the constructor will be used. @param encrypted (bool) Is the connection with the server being encrypted? @return Return a tuple: \n - In case of success: (0 destination_filename)\n - In case of error: (error_code None)\n See here to see the status codes returned:\n https://ec.haxx.se/usingcurl-returns.html \n Note that we return error 20 in case of an unknown error. ''' # Use arguments provided in the constructor if url is None and self.__url is not None: url = self.__url if dst_file is None and self.__dst_file is not None: dst_file = self.__dst_file if incl_http_headers is None and self.__incl_http_headers is not None: incl_http_headers = self.__incl_http_headers if is_secure is None and self.__is_secure is not None: is_secure = self.__is_secure if timeout is None and self.__timeout is not None: timeout = self.__timeout if retry is None and self.__retry is not None: retry = self.__retry if curl_args is None and self.__curl_args is not None: curl_args = self.__curl_args if encrypted is None and self.__encrypted is not None: encrypted = self.__encrypted # We can't run without a URL if url is None: return (-1, dst_file) # If no filename is provided, we use the last part of the url if dst_file is None: dst_file = os.path.basename(url) # If there is no path in the provided filename, we store the file under this default location try: if dst_file.find('/') == -1: dst_file = getCfg('ztp-tmp') + '/' + dst_file except (AttributeError) as e: logger.error("!Exception : %s" % (str(e))) return (20, None) # Create curl command cmd = '/usr/bin/curl -f -v -s -o ' + dst_file if self.__user_agent is not None: cmd += ' -A "' + self.__user_agent + '"' # --user-agent if is_secure is False: cmd += ' -k' # --insecure if timeout is not None and isinstance(timeout, int) is True: cmd += ' --connect-timeout ' + str(timeout) if retry is not None and isinstance(retry, int) is True: cmd += ' --retry ' + str(retry) if incl_http_headers is not None: for h in self.__http_headers: cmd += ' -H \"' + h + '"' # --header if curl_args is not None: cmd += ' ' + curl_args cmd += ' ' + url if verbose is True: logger.debug('%s' % (cmd)) # Execute curl command _retries = retry while True: _start_time = time.time() (rc, cmd_stdout, cmd_stderr) = runCommand(cmd) _current_time = time.time() if rc != 0 and rc in [ 5, 6, 7 ] and _retries != 0 and (_current_time - _start_time) < timeout: logger.debug( "!Error (%d) encountered while processing the command : %s" % (rc, cmd)) time.sleep(timeout - (_current_time - _start_time)) _retries = _retries - 1 continue if rc != 0: logger.error( "!Error (%d) encountered while processing the command : %s" % (rc, cmd)) for l in cmd_stdout: # pragma: no cover logger.error(str(l)) for l in cmd_stderr: logger.error(str(l)) if os.path.isfile(dst_file): os.remove(dst_file) return (20, None) else: break os.chmod(dst_file, stat.S_IRWXU) # Use curl result return (0, dst_file)
def test_cmd(self, tmpdir): (rc1, cmd_stdout1, cmd_stderr1) = runCommand('/bin/cat /proc/devices') (rc2, cmd_stdout2, cmd_stderr2) = runCommand('/bin/cat /proc/devices', use_shell=True) (rc3, cmd_stdout3, cmd_stderr3) = runCommand(['/bin/cat', '/proc/devices']) (rc4, cmd_stdout4, cmd_stderr4) = runCommand(['/bin/cat', '/proc/devices'], use_shell=True) assert ((rc1 == rc2) and (rc2 == rc3) and (rc3 == rc4)) assert ((cmd_stdout1 == cmd_stdout2) and (cmd_stdout2 == cmd_stdout3) and (cmd_stdout3 == cmd_stdout4)) assert ((cmd_stderr1 == cmd_stderr2) and (cmd_stderr2 == cmd_stderr3) and (cmd_stderr3 == cmd_stderr4)) (rc1, cmd_stdout1, cmd_stderr1) = runCommand('ps hjk') (rc2, cmd_stdout2, cmd_stderr2) = runCommand('ps hjk', use_shell=True) (rc3, cmd_stdout3, cmd_stderr3) = runCommand(['ps', 'hjk']) (rc4, cmd_stdout4, cmd_stderr4) = runCommand(['ps', 'hjk'], use_shell=True) assert ((rc1 == rc2) and (rc2 == rc3) and (rc3 == rc4)) assert ((cmd_stdout1 == cmd_stdout2) and (cmd_stdout2 == cmd_stdout3) and (cmd_stdout3 == cmd_stdout4)) assert ((cmd_stderr1 == cmd_stderr2) and (cmd_stderr2 == cmd_stderr3) and (cmd_stderr3 == cmd_stderr4)) d = tmpdir.mkdir("valid") fh = d.join("test.sh") fh.write("""#!/bin/sh echo $1 echo $2 > /dev/stderr exit $# """) os.chmod(str(fh), stat.S_IRWXU) (rc1, cmd_stdout1, cmd_stderr1) = runCommand(str(fh) + ' ABC XYZ') (rc2, cmd_stdout2, cmd_stderr2) = runCommand(str(fh) + ' ABC XYZ', use_shell=True) (rc3, cmd_stdout3, cmd_stderr3) = runCommand([str(fh), 'ABC', 'XYZ']) (rc4, cmd_stdout4, cmd_stderr4) = runCommand([str(fh), 'ABC', 'XYZ'], use_shell=True) assert ((rc1 == rc2) and (rc2 == rc3) and (rc3 == rc4)) assert ((cmd_stdout1 == cmd_stdout2) and (cmd_stdout2 == cmd_stdout3) and (cmd_stdout3 == cmd_stdout4)) assert ((cmd_stderr1 == cmd_stderr2) and (cmd_stderr2 == cmd_stderr3) and (cmd_stderr3 == cmd_stderr4))
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 __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)
def test_json_config_valid_reload(self, tmpdir): '''! Test case when the new json configuration file to apply is syntaxically valid. Verify that the new configuration is correctly applied using 'config reload'. Verify that the plugin does not return with a non zero exit code. ''' d = tmpdir.mkdir("valid") fh_before = d.join("config-before.json") cmd = 'config save -y ' + str(fh_before) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0) cmd = 'sonic-cfggen -d -H -v DEVICE_METADATA.localhost.platform' (rc, cmd_stdout, cmd_stderr) = runCommand(cmd) assert (rc == 0) platform = cmd_stdout[0] cmd = 'sonic-cfggen -d -H -v DEVICE_METADATA.localhost.hwsku' (rc, cmd_stdout, cmd_stderr) = runCommand(cmd) assert (rc == 0) hwsku = cmd_stdout[0] sku_dir = '/usr/share/sonic/device/{}/{}'.format(platform, hwsku) os.system('rm -rf {}'.format(sku_dir + '_dup')) os.system('cp -R {} {}'.format(sku_dir, sku_dir + '_dup')) data = self.__read_file(str(fh_before)) fh_after = d.join("config-after.json") self.__write_file(str(fh_after), data) cfgDict = {} with open(str(fh_after)) as fp: cfgDict = json.load(fp) cfgDict['DEVICE_METADATA']['localhost']['hostname'] = "something" cfgDict['DEVICE_METADATA']['localhost']['platform'] = "invalid1" cfgDict['DEVICE_METADATA']['localhost']['mac'] = "invalid2" cfgDict['DEVICE_METADATA']['localhost']['hwsku'] = hwsku + "_dup" with open(str(fh_after), "w") as fp: json.dump(cfgDict, fp, indent=4) fh = d.join("input.json") fh.write(""" { "configdb-json": { "clear-config": true, "halt-on-failure": false, "ignore-result": false, "reboot-on-failure": false, "reboot-on-success": false, "status": "BOOT", "timestamp": "2019-05-01 19:49:25", "url": { "destination": "/etc/sonic/config_db.json", "source": "file://%s" } } } """ % (str(fh_after))) configdb_json = ConfigDBJson(str(fh)) configdb_json.main() fh_after = d.join("config-after.json") cmd = 'config save -y ' + str(fh_after) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0) with open(str(fh_after)) as json_file: json_dict = json.load(json_file) assert (json_dict.get('DEVICE_METADATA').get('localhost').get( 'platform') != 'invalid1') assert ( json_dict.get('DEVICE_METADATA').get('localhost').get('mac') != 'invalid2') assert (json_dict.get('DEVICE_METADATA').get('localhost').get( 'hwsku') == hwsku + "_dup") # Restore initial configuration cmd = 'config reload -y -f ' + str(fh_before) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0) rc = runCommand('config save -y', capture_stdout=False)
def test_json_config_valid_load(self, tmpdir): '''! Test case when the new json configuration file to apply is syntaxically valid. Verify that the new configuration is correctly applied using 'config load'. Verify that the plugin does not return with a non zero exit code. ''' d = tmpdir.mkdir("valid") fh_before = d.join("config-before.json") cmd = 'config save -y ' + str(fh_before) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0) data = self.__read_file(str(fh_before)) fh_after = d.join("config-after.json") self.__write_file(str(fh_after), data) cfgDict = {} with open(str(fh_after)) as fp: cfgDict = json.load(fp) cfgDict['DEVICE_METADATA']['localhost']['hostname'] = "something" cfgDict['DEVICE_METADATA']['localhost']['platform'] = "invalid1" cfgDict['DEVICE_METADATA']['localhost']['mac'] = "invalid2" cfgDict['DEVICE_METADATA']['localhost']['hwsku'] = "invalid3" with open(str(fh_after), "w") as fp: json.dump(cfgDict, fp, indent=4) fh = d.join("input.json") fh.write(""" { "configdb-json": { "clear-config": false, "halt-on-failure": false, "ignore-result": false, "reboot-on-failure": false, "reboot-on-success": false, "status": "BOOT", "timestamp": "2019-05-01 19:49:25", "url": { "destination": "/etc/sonic/config_db.json", "source": "file://%s" } } } """ % (str(fh_after))) configdb_json = ConfigDBJson(str(fh)) configdb_json.main() fh_after = d.join("config-after.json") cmd = 'config save -y ' + str(fh_after) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0) with open(str(fh_after)) as json_file: json_dict = json.load(json_file) assert (json_dict.get('DEVICE_METADATA').get('localhost').get( 'platform') != 'invalid1') assert ( json_dict.get('DEVICE_METADATA').get('localhost').get('mac') != 'invalid2') assert ( json_dict.get('DEVICE_METADATA').get('localhost').get('hwsku') != 'invalid3') # Collect the differences between the two configurations cmd = "/usr/bin/diff --changed-group-format='%>' --unchanged-group-format='' " + str( fh_before) + ' ' + str(fh_after) (rc2, cmd_stdout, cmd_stderr) = runCommand(cmd) # Restore initial configuration cmd = 'config load -y ' + str(fh_before) rc = runCommand(cmd, capture_stdout=False) assert (rc == 0)