def __add_tag__(tag_name, list_of_objs): """Returns list_of_objs with tag_name added to each object""" # Create tag_to_add tag_to_add = {"tag_handle": tag_name, "value": None} err_msg = "Error adding tag '{0}'. '{1}' (printed above) is not a {2}. Instead a {3} was provided.\nProvided ImportDefinition in the customize.py file may be corrupt" # Check list_of_objs is a List if not isinstance(list_of_objs, list): LOG.error("Error adding tag.\n'list_of_objs': %s", list_of_objs) raise ExtException( err_msg.format(tag_name, "list_of_objs", "List", type(list_of_objs))) # Loop each object in the List for obj in list_of_objs: # If its not a dict, error if not isinstance(obj, dict): LOG.error("Error adding tag.\n'list_of_objs': %s\n'obj': %s", list_of_objs, obj) raise ExtException( err_msg.format(tag_name, "obj", "Dictionary", type(obj))) # Try get current_tags current_tags = obj.get("tags") # If None, create new empty List if current_tags is None: current_tags = [] # If current_tags is not a list, error if not isinstance(current_tags, list): LOG.error("Error adding tag.\n'current_tags': %s", current_tags) raise ExtException( err_msg.format(tag_name, "current_tags", "List", type(current_tags))) # Append our tag_to_add to current_tags current_tags.append(tag_to_add) # Set the obj's 'tags' value to current_tags obj["tags"] = current_tags # Return the updated list_of_objs return list_of_objs
def __validate_directory__(permissions, path_to_dir): """Check the given path is absolute, exists and has the given permissions, else raises an Exception""" # Check the path is absolute if not os.path.isabs(path_to_dir): raise ExtException( "The path to the directory must be an absolute path: {0}". format(path_to_dir)) # Check the directory exists if not os.path.isdir(path_to_dir): raise ExtException( "The path does not exist: {0}".format(path_to_dir)) # Check we have the correct permissions Ext.__has_permissions__(permissions, path_to_dir)
def __get_tar_file_path_to_extract__(tar_members, file_name): """Loop all the tar_members and return the path to the member that matcheds file_name. Raise an Exception if cannot find file_name in the tar package""" for member in tar_members: tar_file_name = os.path.split(member.name) if tar_file_name[1] == file_name: return member.name raise ExtException( "Invalid built distribution. Could not find {0}".format(file_name))
def __validate_file_paths__(permissions=None, *args): """Check the given *args paths exist and has the given permissions, else raises an Exception""" # For each *args for path_to_file in args: # Check the file exists if not os.path.isfile(path_to_file): raise ExtException( "Could not find file: {0}".format(path_to_file)) if permissions: # Check we have the correct permissions Ext.__has_permissions__(permissions, path_to_file)
def __has_permissions__(permissions, path): """Raises an exception if the user does not have the given permissions to path""" if not os.access(path, permissions): if permissions is os.R_OK: permissions = "READ" elif permissions is os.W_OK: permissions = "WRITE" raise ExtException( "User does not have {0} permissions for: {1}".format( permissions, path))
def __parse_setup_py__(path, attribute_names): """Parse the values of the given attribute_names and return a Dictionary attribute_name:attribute_value""" # Read the setup.py file into a List setup_py_lines = ExtCreate.__read_file__(path) # Raise an error if nothing found in the file if not setup_py_lines: raise ExtException( "No content found in provided setup.py file: {0}".format(path)) setup_regex_pattern = r"setup\(" setup_defined, index_of_setup, return_dict = False, None, dict() for i in range(len(setup_py_lines)): if re.match(pattern=setup_regex_pattern, string=setup_py_lines[i]) is not None: setup_defined = True index_of_setup = i break # Raise an error if we can't find 'setup()' in the file if not setup_defined: raise ExtException( "Could not find 'setup()' defined in provided setup.py file: {0}" .format(path)) # Get sublist containing lines from 'setup(' to EOF + trim whitespaces setup_py_lines = setup_py_lines[index_of_setup:] setup_py_lines = [file_line.strip() for file_line in setup_py_lines] # Foreach attribute_name, get its value and add to return_dict for attribute_name in attribute_names: return_dict[attribute_name] = ExtCreate.__parse_setup_attribute__( path, setup_py_lines, attribute_name) return return_dict
def convert_to_extension(cls, path_built_distribution, custom_display_name=None): """ Function that converts an (old) Integration into a Resilient Extension. Validates then converts the given built_distribution (either .tar.gz or .zip). Returns the path to the new Extension.zip - path_built_distribution [String]: - If a .tar.gz: must include a setup.py, customize.py and config.py file. - If a .zip: must include a valid .tar.gz. - custom_display_name [String]: will give the Extension that display name. Default: name from setup.py file - The Extension.zip will be produced in the same directory as path_built_distribution""" LOG.info("Converting extension from: %s", path_built_distribution) path_tmp_built_distribution, path_extracted_tar = None, None # Dict of the required files we need to try extract in order to create an Extension extracted_required_files = { "setup.py": None, "customize.py": None, "config.py": None } # Raise Exception if the user tries to pass a Directory if os.path.isdir(path_built_distribution): raise ExtException( "You must specify a Built Distribution. Not a Directory\nDirectory Specified: {0}" .format(path_built_distribution)) # Raise Exception if not a .tar.gz or .zip file if not os.path.isfile(path_built_distribution) or ( not tarfile.is_tarfile(path_built_distribution) and not zipfile.is_zipfile(path_built_distribution)): raise ExtException( "File corrupt. Supported Built Distributions are .tar.gz and .zip\nInvalid Built Distribution provided: {0}" .format(path_built_distribution)) # Validate we can read the built distribution cls.__validate_file_paths__(os.R_OK, path_built_distribution) # Create a tmp directory path_tmp_dir = tempfile.mkdtemp(prefix="resilient-circuits-tmp-") try: # Copy built distribution to tmp directory shutil.copy(path_built_distribution, path_tmp_dir) # Get the path of the built distribution in the tmp directory path_tmp_built_distribution = os.path.join( path_tmp_dir, os.path.split(path_built_distribution)[1]) # Handle if it is a .tar.gz file if tarfile.is_tarfile(path_tmp_built_distribution): LOG.info( "A .tar.gz file was provided. Will now attempt to convert it to a Resilient Extension." ) # Extract the required files to the tmp dir and return a dict of their paths extracted_required_files = cls.__get_required_files_from_tar_file__( path_tar_file=path_tmp_built_distribution, dict_required_files=extracted_required_files, output_dir=path_tmp_dir) path_extracted_tar = path_tmp_built_distribution # Handle if is a .zip file elif zipfile.is_zipfile(path_tmp_built_distribution): LOG.info( "A .zip file was provided. Will now attempt to convert it to a Resilient Extension." ) with zipfile.ZipFile(file=path_tmp_built_distribution, mode="r") as zip_file: # Get a List of all the members of the zip file (including files in directories) zip_file_members = zip_file.infolist() LOG.info("\nValidating Built Distribution: %s", path_built_distribution) # Loop the members for zip_member in zip_file_members: LOG.info("\t- %s", zip_member.filename) # Extract the member path_extracted_member = zip_file.extract( member=zip_member, path=path_tmp_dir) # Handle if the member is a directory if os.path.isdir(path_extracted_member): LOG.debug( "\t\t- Is a directory.\n\t\t- Skipping...") # delete the extracted member shutil.rmtree(path_extracted_member) continue # Handle if it is a .tar.gz file elif tarfile.is_tarfile(path_extracted_member): LOG.info("\t\t- Is a .tar.gz file!") # Set the path to the extracted .tar.gz file path_extracted_tar = path_extracted_member # Try to extract the required files from the .tar.gz try: extracted_required_files = cls.__get_required_files_from_tar_file__( path_tar_file=path_extracted_member, dict_required_files= extracted_required_files, output_dir=path_tmp_dir) LOG.info( "\t\t- Found files: %s\n\t\t- Its path: %s\n\t\t- Is a valid Built Distribution!", ", ".join(extracted_required_files.keys()), path_extracted_tar) break except ExtException as err: # If "invalid" is in the error message, # then we did not find one of the required files in the .tar.gz # so we warn the user, delete the extracted member and continue the loop if "invalid" in err.message.lower(): LOG.warning( "\t\t- Failed to extract required files: %s\n\t\t- Invalid format.\n%s", ", ".join( extracted_required_files.keys()), err.message) os.remove(path_extracted_member) else: raise ExtException(err) # Handle if it is a regular file elif os.path.isfile(path_extracted_member): # Get the file name file_name = os.path.basename(path_extracted_member) # If the file is a required one, add its path to the dict if file_name in extracted_required_files: LOG.info("\t\t- Found %s file", file_name) extracted_required_files[ file_name] = path_extracted_member # Set the path to extracted tar to this zip file path_extracted_tar = zip_file.filename # Else its some other file, so skip else: LOG.debug( "\t\t- It is not a .tar.gz file\n\t\t- Skipping..." ) os.remove(path_extracted_member) # if extracted_required_files contains values for all required files, then break if all(extracted_required_files.values()): LOG.info( "\t\t- This is a valid Built Distribution!") break else: LOG.debug( "\t\t- Is not a valid .tar.gz built distribution\n\t\t- Skipping..." ) # If we could not get all the required files to create an Extension, raise an error if not all(extracted_required_files.values()): raise ExtException( "Could not extract required files from given Built Distribution\nRequired Files: {0}\nDistribution: {1}" .format(", ".join(extracted_required_files.keys()), path_built_distribution)) # Create the extension path_tmp_the_extension_zip = cls.create_extension( path_setup_py_file=extracted_required_files.get("setup.py"), path_customize_py_file=extracted_required_files.get( "customize.py"), path_config_py_file=extracted_required_files.get("config.py"), output_dir=path_tmp_dir, path_built_distribution=path_extracted_tar, custom_display_name=custom_display_name) # Copy the extension.zip to the same directory as the original built distribution shutil.copy(path_tmp_the_extension_zip, os.path.dirname(path_built_distribution)) # Get the path to the final extension.zip path_the_extension_zip = os.path.join( os.path.dirname(path_built_distribution), os.path.basename(path_tmp_the_extension_zip)) LOG.info("Extension created at: %s", path_the_extension_zip) return path_the_extension_zip except Exception as err: raise ExtException(err) finally: # Remove the tmp directory shutil.rmtree(path_tmp_dir)
def __get_import_definition_from_customize_py__(path_customize_py_file): """Return the base64 encoded ImportDefinition in a customize.py file as a Dictionary""" # Insert the customize.py parent dir to the start of our Python PATH at runtime so we can import the customize module from within it path_to_util_dir = os.path.dirname(path_customize_py_file) sys.path.insert(0, path_to_util_dir) # Import the customize module customize_py = importlib.import_module("customize") # Reload the module so we get the latest one # If we do not reload, can get stale results if # this method is called more then once reload(customize_py) # Call customization_data() to get all ImportDefinitions that are "yielded" customize_py_import_definitions_generator = customize_py.customization_data( ) customize_py_import_definitions = [] # customization_data() returns a Generator object with all yield statements, so we loop them for definition in customize_py_import_definitions_generator: if isinstance(definition, ImportDefinition): customize_py_import_definitions.append( json.loads(base64.b64decode(definition.value))) else: LOG.warning( "WARNING: Unsupported data found in customize.py file. Expected an ImportDefinition. Got: '%s'", definition) # If no ImportDefinition found if not customize_py_import_definitions: raise ExtException( "No ImportDefinition found in the customize.py file") # If more than 1 found elif len(customize_py_import_definitions) > 1: raise ExtException( "Multiple ImportDefinitions found in the customize.py file. There must only be 1 ImportDefinition defined" ) # Get the import defintion as dict customize_py_import_definition = customize_py_import_definitions[0] # Get reference to incident_types if there are any incident_types = customize_py_import_definition.get( "incident_types", []) if incident_types: incident_type_to_remove = None # Loop through and remove this custom one (that is originally added using codegen) for incident_type in incident_types: if incident_type.get("uuid") == DEFAULT_INCIDENT_TYPE_UUID: incident_type_to_remove = incident_type break if incident_type_to_remove: incident_types.remove(incident_type_to_remove) # Remove the path from PYTHONPATH sys.path.remove(path_to_util_dir) return customize_py_import_definition
def create_extension(cls, path_setup_py_file, path_customize_py_file, path_config_py_file, output_dir, path_built_distribution=None, path_extension_logo=None, path_company_logo=None, custom_display_name=None, keep_build_dir=False): """ Function that creates The Extension.zip file from the given setup.py, customize.py and config.py files and copies it to the output_dir. Returns the path to the Extension.zip - path_setup_py_file [String]: abs path to the setup.py file - path_customize_py_file [String]: abs path to the customize.py file - path_config_py_file [String]: abs path to the config.py file - output_dir [String]: abs path to the directory the Extension.zip should be produced - path_built_distribution [String]: abs path to a tar.gz Built Distribution - if provided uses that .tar.gz - else looks for it in the output_dir. E.g: output_dir/package_name.tar.gz - path_extension_logo [String]: abs path to the extension_logo.png. Has to be 200x72 and a .png file - if not provided uses default icon - path_company_logo [String]: abs path to the extension_logo.png. Has to be 100x100 and a .png file - if not provided uses default icon - custom_display_name [String]: will give the Extension that display name. Default: name from setup.py file - keep_build_dir [Boolean]: if True, build/ will not be remove. Default: False """ LOG.info("Creating Extension") # Ensure the output_dir exists, we have WRITE access and ensure we can READ setup.py and customize.py cls.__validate_directory__(os.W_OK, output_dir) cls.__validate_file_paths__(os.R_OK, path_setup_py_file, path_customize_py_file) # Parse the setup.py file setup_py_attributes = cls.__parse_setup_py__( path_setup_py_file, cls.supported_setup_py_attribute_names) # Validate setup.py attributes # Validate the name attribute. Raise exception if invalid if not cls.__is_valid_package_name__(setup_py_attributes.get("name")): raise ExtException( "'{0}' is not a valid Extension name. The name attribute must be defined and can only include 'a-z and _'.\nUpdate this value in the setup.py file located at: {1}" .format(setup_py_attributes.get("name"), path_setup_py_file)) # Validate the version attribute. Raise exception if invalid if not cls.__is_valid_version_syntax__( setup_py_attributes.get("version")): raise ExtException( "'{0}' is not a valid Extension version syntax. The version attribute must be defined. Example: version=\"1.0.0\".\nUpdate this value in the setup.py file located at: {1}" .format(setup_py_attributes.get("version"), path_setup_py_file)) # Validate the url supplied in the setup.py file, set to an empty string if not valid if not cls.__is_valid_url__(setup_py_attributes.get("url")): LOG.warning("WARNING: '%s' is not a valid url. Ignoring.", setup_py_attributes.get("url")) setup_py_attributes["url"] = "" # Get ImportDefinition from customize.py customize_py_import_definition = cls.__get_import_definition_from_customize_py__( path_customize_py_file) # Get the tag name tag_name = setup_py_attributes.get("name") # Add the tag to the import defintion customize_py_import_definition = cls.__add_tag_to_import_definition__( tag_name, cls.supported_res_obj_names, customize_py_import_definition) # Parse the app.configs from the config.py file app_configs = cls.__get_configs_from_config_py__(path_config_py_file) # Generate the name for the extension extension_name = "{0}-{1}".format(setup_py_attributes.get("name"), setup_py_attributes.get("version")) # Generate paths to the directories and files we will use in the build directory path_build = os.path.join(output_dir, BASE_NAME_BUILD) path_extension_json = os.path.join(path_build, BASE_NAME_EXTENSION_JSON) path_export_res = os.path.join(path_build, BASE_NAME_EXPORT_RES) path_executables = os.path.join(path_build, BASE_NAME_EXECUTABLES) path_executable_zip = os.path.join( path_executables, "{0}{1}".format(PREFIX_EXECUTABLE_ZIP, extension_name)) path_executable_json = os.path.join(path_executable_zip, BASE_NAME_EXECUTABLE_JSON) path_executable_dockerfile = os.path.join( path_executable_zip, BASE_NAME_EXECUTABLE_DOCKERFILE) try: # If there is an old build directory, remove it first if os.path.exists(path_build): shutil.rmtree(path_build) # Create the directories for the path "/build/executables/exe-<package-name>/" os.makedirs(path_executable_zip) # If no path_built_distribution is given, use the default: "<output_dir>/<package-name>.tar.gz" if not path_built_distribution: path_built_distribution = os.path.join( output_dir, "{0}.tar.gz".format(extension_name)) # Validate the built distribution exists and we have READ access cls.__validate_file_paths__(os.R_OK, path_built_distribution) # Copy the built distribution to the executable_zip dir and enforce rename to .tar.gz shutil.copy( path_built_distribution, os.path.join(path_executable_zip, "{0}.tar.gz".format(extension_name))) # Generate the contents for the executable.json file the_executable_json_file_contents = {"name": extension_name} # Write the executable.json file cls.__write_file__( path_executable_json, json.dumps(the_executable_json_file_contents, sort_keys=True)) # NOTE: Dockerfile creation commented out for this release ''' # Load Dockerfile template docker_file_template = cls.jinja_env.get_template(JINJA_TEMPLATE_DOCKERFILE) # Render Dockerfile template with required variables the_dockerfile_contents = docker_file_template.render({ "extension_name": extension_name, "installed_package_name": setup_py_attributes.get("name").replace("_", "-"), "app_configs": app_configs[1] }) # Write the Dockerfile cls.__write_file__(path_executable_dockerfile, the_dockerfile_contents) ''' # zip the executable_zip dir shutil.make_archive(base_name=path_executable_zip, format="zip", root_dir=path_executable_zip) # Remove the executable_zip dir shutil.rmtree(path_executable_zip) # Get the extension_logo (icon) and company_logo (author.icon) as base64 encoded strings extension_logo = cls.__get_icon__( icon_name=os.path.basename(PATH_DEFAULT_ICON_EXTENSION_LOGO), path_to_icon=path_extension_logo, width_accepted=200, height_accepted=72, default_path_to_icon=PATH_DEFAULT_ICON_EXTENSION_LOGO) company_logo = cls.__get_icon__( icon_name=os.path.basename(PATH_DEFAULT_ICON_COMPANY_LOGO), path_to_icon=path_company_logo, width_accepted=100, height_accepted=100, default_path_to_icon=PATH_DEFAULT_ICON_COMPANY_LOGO) # Generate the contents for the extension.json file the_extension_json_file_contents = { "author": { "name": setup_py_attributes.get("author"), "website": setup_py_attributes.get("url"), "icon": { "data": company_logo, "media_type": "image/png" } }, "description": { "content": setup_py_attributes.get("description"), "format": "text" }, "display_name": custom_display_name if custom_display_name is not None else setup_py_attributes.get("name"), "icon": { "data": extension_logo, "media_type": "image/png" }, "long_description": { "content": "<div>{0}</div>".format( setup_py_attributes.get("long_description")), "format": "html" }, "minimum_resilient_version": { "major": customize_py_import_definition.get("server_version").get( "major"), "minor": customize_py_import_definition.get("server_version").get( "minor"), "build_number": customize_py_import_definition.get("server_version").get( "build_number"), "version": customize_py_import_definition.get("server_version").get( "version") }, "name": setup_py_attributes.get("name"), "tag": { "prefix": tag_name, "name": tag_name, "display_name": tag_name, "uuid": cls.__generate_uuid_from_string__(tag_name) }, "uuid": cls.__generate_uuid_from_string__("{0}-{1}".format( setup_py_attributes.get("name"), setup_py_attributes.get("version"))), "version": setup_py_attributes.get("version"), # TODO: discuss with Sasquatch. Can add the app_config_str here, but will not install # TODO: get 'Unrecognized field "app_config_str"' error # "app_config_str": app_configs[0] } # Write the executable.json file cls.__write_file__( path_extension_json, json.dumps(the_extension_json_file_contents, sort_keys=True)) # Write the customize ImportDefinition to the export.res file cls.__write_file__( path_export_res, json.dumps(customize_py_import_definition, sort_keys=True)) # Copy the built distribution to the build dir, enforce rename to .tar.gz shutil.copy( path_built_distribution, os.path.join(path_build, "{0}.tar.gz".format(extension_name))) # create The Extension Zip by zipping the build directory extension_zip_base_path = os.path.join( output_dir, "{0}{1}".format(PREFIX_EXTENSION_ZIP, extension_name)) extension_zip_name = shutil.make_archive( base_name=extension_zip_base_path, format="zip", root_dir=path_build) path_the_extension_zip = os.path.join(extension_zip_base_path, extension_zip_name) except ExtException as err: raise err except Exception as err: raise ExtException(err) finally: # Remove the executable_zip dir. Keep it if user passes --keep-build-dir if not keep_build_dir: shutil.rmtree(path_build) LOG.info("Extension %s created", "{0}{1}".format(PREFIX_EXTENSION_ZIP, extension_name)) # Return the path to the extension zip return path_the_extension_zip
def __get_icon__(icon_name, path_to_icon, width_accepted, height_accepted, default_path_to_icon): """Returns the icon at path_to_icon as a base64 encoded string if it is a valid .png file with the resolution width_accepted x height_accepted. If path_to_icon does not exist, default_path_to_icon is returned as a base64 encoded string""" path_icon_to_use = path_to_icon # Use default_path_to_icon if path_to_icon does not exist if not path_icon_to_use or not os.path.isfile(path_icon_to_use): LOG.warning( "WARNING: Default Extension Icon will be used\nProvided custom icon path for %s is invalid: %s\nNOTE: %s should be placed in the /icons directory", icon_name, path_icon_to_use, icon_name) path_icon_to_use = default_path_to_icon else: LOG.info("INFO: Using custom %s icon: %s", icon_name, path_icon_to_use) # Validate path_icon_to_use and ensure we have READ permissions try: ExtCreate.__validate_file_paths__(os.R_OK, path_icon_to_use) except ExtException as err: raise OSError( "Could not find valid icon file. Looked at two locations:\n{0}\n{1}\n{2}" .format(path_to_icon, default_path_to_icon, err.message)) # Get the extension of the file. os.path.splitext returns a Tuple with the file extension at position 1 and can be an empty string split_path = os.path.splitext(path_icon_to_use) file_extension = split_path[1] if not file_extension: raise ExtException( "Provided icon file does not have an extension. Icon file must be .png\nIcon File: {0}" .format(path_icon_to_use)) elif file_extension != ".png": raise ExtException( "{0} is not a supported icon file type. Icon file must be .png\nIcon File: {1}" .format(file_extension, path_icon_to_use)) # Open the icon_file in Bytes mode to validate its resolution with open(path_icon_to_use, mode="rb") as icon_file: # According to: https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_format # First need to seek 16 bytes: # 8 bytes: png signature # 4 bytes: IDHR Chunk Length # 4 bytes: IDHR Chunk type icon_file.seek(16) try: # Bytes 17-20 = image width. Use struct to unpack big-endian encoded unsigned int icon_width = struct.unpack(">I", icon_file.read(4))[0] # Bytes 21-24 = image height. Use struct to unpack big-endian encoded unsigned int icon_height = struct.unpack(">I", icon_file.read(4))[0] except Exception as err: raise ExtException( "Failed to read icon's resolution. Icon file corrupt. Icon file must be .png\nIcon File: {0}" .format(path_icon_to_use)) # Raise exception if resolution is not accepted if icon_width != width_accepted or icon_height != height_accepted: raise ExtException( "Icon resolution is {0}x{1}. Resolution must be {2}x{3}\nIcon File:{4}" .format(icon_width, icon_height, width_accepted, height_accepted, path_icon_to_use)) # If we get here all validations have passed. Open the file in Bytes mode and encode it as base64 and decode to a utf-8 string with open(path_icon_to_use, "rb") as icon_file: encoded_icon_string = base64.b64encode( icon_file.read()).decode("utf-8") return encoded_icon_string
def __get_configs_from_config_py__(path_config_py_file): """Returns a tuple (config_str, config_list). If no configs found, return ("", []). Raises Exception if it fails to parse configs - config_str: is the full string found in the config.py file - config_list: is a list of dict objects that contain each un-commented config - Each dict object has the attributes: name, placeholder, env_name, section_name """ config_str, config_list = "", [] # Insert the customize.py parent dir to the start of our Python PATH at runtime so we can import the customize module from within it path_to_util_dir = os.path.dirname(path_config_py_file) sys.path.insert(0, path_to_util_dir) try: # Import the config module config_py = importlib.import_module("config") # Reload the module so we get the latest one # If we do not reload, can get stale results if # this method is called more then once reload(config_py) # Call config_section_data() to get the string containing the configs config_str = config_py.config_section_data() # Instansiate a new configparser config_parser = configparser.ConfigParser() # Read and parse the configs from the config_str if sys.version_info < (3, 2): # config_parser.readfp() was deprecated and replaced with read_file in PY3.2 config_parser.readfp(io.StringIO(config_str)) else: config_parser.read_file(io.StringIO(config_str)) # Get the configs from each section for section_name in config_parser.sections(): parsed_configs = config_parser.items(section_name) for config in parsed_configs: config_list.append({ "name": config[0], "placeholder": config[1], "env_name": "{0}_{1}".format(section_name.upper(), config[0].upper()), "section_name": section_name }) except Exception as err: raise ExtException( "Failed to parse configs from config.py file\nThe config.py file may be corrupt. Visit the App Exchange to contact the developer\nReason: {0}" .format(err)) finally: # Remove the path from PYTHONPATH sys.path.remove(path_to_util_dir) return (config_str, config_list)