#!/usr/bin/env python3 # # ex:ts=4:sw=4:sts=4:et # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- # # Security Response Tool Implementation # # Copyright (C) 2019 Wind River Systems # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License along # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # Theory of operation # # This script supports extending common/template code with customized # sections, with support for merging and upstreaming changes in the # common/shared sections of the parent upstream files. # # The initial use case is a shared Jira integration file that partners can # extend to their particular installation. A working example is provided in # the ACME directory: # upstream: "bin/common/srtool_jira_template.py" # custom : "bin/acme/srtool_jira_acme.py" # # The custom sections are blocked off with comment tags (e.g. ACME): # ### ACME_EXTENSION_BEGIN ### # ... # ### ACME_EXTENSION_END ### # # The script supports JSON files to predefine that mappings. See: # bin/acme/patcher.json # # Examples: # Merge shared upstream code into a custom Jira script: # $ ./bin/common/srtool_patcher.py -j bin/acme/patcher.json --merge-original # Merge edits in script's common code areas back to upstream: # $ ./bin/common/srtool_patcher.py -j bin/acme/patcher.json --merge-custom # See if the shared common code sections have diverged from upstream: # $ ./bin/common/srtool_patcher.py -j bin/acme/patcher.json --diff-original # Extract the custom code sections, for examining and preserving custom changes: # $ ./bin/common/srtool_patcher.py -j bin/acme/patcher.json --extract-custom-patch # import os import sys import argparse import json # Setup: verbose = False INPLACE_TAG = 'INPLACE' DISABLE_TAG = 'DISABLE' ################################# # extract_custom_patch # # Extract a clean version of the file without the the custom sections, # and generate a patch file of those sections # def extract_custom_patch(custom_file,clean_file,patch_file,label,patcher_dir,options): tag_begin = "%s_EXTENSION_BEGIN" % label tag_end = "%s_EXTENSION_END" % label ret = 0 # Insure patcher working directory is ready if not patcher_dir: patcher_dir = os.path.dirname(custom_file) try: os.makedirs(patcher_dir) except: pass # Prepare defaults if not clean_file: clean_file = os.path.join(patcher_dir,os.path.basename(custom_file) + '.clean') if not patch_file: patch_file = os.path.join(patcher_dir,os.path.basename(custom_file) + '.patch') with open(custom_file, 'r') as fs: with open(clean_file, 'w') as fd: state = "find" for line in fs: #print("LINE(%s):%s" % (state,line.strip())) if state == "find": if 0 < line.find(tag_begin): #print("START:$line") state = "found" continue elif state == "found": if 0 < line.find(tag_end): #print("STOP:$line") state = "blank" continue elif state == "blank": state = "find" # if next line after stop is blank, hide that also if not line.strip(): continue # Normal line? if state != "found": fd.write('%s' % line) # Did we end cleanly? if state != "find": print("ERROR: START not STOPPED (%s)" % state) if INPLACE_TAG in options: cmd = "git diff %s > %s" % (custom_file,patch_file) print(cmd) os.system(cmd) print("In-place File: %s" % (clean_file)) else: os.system("diff -u %s %s > %s" % (clean_file,custom_file,patch_file)) print("Custom File: %s" % (custom_file)) print("Clean File: %s" % (clean_file)) print("Patch File: %s" % (patch_file)) return(ret,clean_file,patch_file) ################################# # merge_original # # Merge any changes in the original upstream file into the # custom file and reapply the custom patches, so that general upstream # changes be be included in the local custom file # def merge_original(custom_file,original_file,patch_file,label,patcher_dir,options): # Skip if in-place if INPLACE_TAG in options: return 0 ret,clean_file,patch_file = extract_custom_patch(custom_file,'',patch_file,label,patcher_dir,options) custom_saved = os.path.join(os.path.dirname(patch_file),os.path.basename(custom_file) + '.saved') if 0 == ret: print("* Preserving custom file as '%s'" % custom_saved) cmd = "cp %s %s" % (custom_file,custom_saved) print(cmd) os.system(cmd) print("* Merging original file '%s' into custom file" % original_file) cmd = "cp %s %s" % (original_file,custom_file) print(cmd) os.system(cmd) cmd = "patch %s %s" % (custom_file,patch_file) print(cmd) ret = os.system(cmd) if 0 != ret: print("* ERROR: Merge failed, restoring previous custom file") cmd = "cp %s %s" % (custom_saved,custom_file) print(cmd) os.system(cmd) return(ret) ################################# # merge_custom # # Merge any changes in the normal non-custom sections of the custom file back into # original file, so that general fixes can be shared upstream # def merge_custom(custom_file,original_file,patch_file,label,patcher_dir,options): # Skip if in-place if INPLACE_TAG in options: return 0 ret,clean_file,patch_file = extract_custom_patch(custom_file,'',patch_file,label,patcher_dir,options) if 0 == ret: print("== Copying clean version of custom file to '%s' ===" % original_file) cmd = "cp -f %s %s" % (clean_file, original_file) print(cmd) os.system(cmd) return(ret) ################################# # diff_original # # Show how the general sections of the custom file compare to the # original file, so that the user can (a) see might be shared upstream, # and/or (b) insure the custom sections are correct and complete # def diff_original(custom_file,original_file,patch_file,label,patcher_dir,options): # Skip if in-place if INPLACE_TAG in options: return 0 ret,clean_file,patch_file = extract_custom_patch(custom_file,'',patch_file,label,patcher_dir,options) if 0 == ret: print("== DIFF from '%s' to clean version of custom file ===" % original_file) os.system("diff -u %s %s" % (original_file,clean_file)) return(ret) ################################# # clean_inplace # # Stash the customized general file and replace it with a clean version # without the custom sections, so that general fixes can be shared upstream # def clean_inplace(custom_file,original_file,patch_file,label,patcher_dir,options): print("FOO1:%s,%s" % (INPLACE_TAG,options)) # Skip if not in-place if not INPLACE_TAG in options: return 0 print("FOO2") # Only continue if 'original' file has custom patches, in case this # command is run multiple times by accident cmd = "grep %s %s" % (label,original_file) ret = os.system(cmd) if 0 != ret: print("ERROR:SKIP: original file '%s' does not have custom tags '%s'" % (original_file,label)) return ret print("* Backup customized in-place file") try: os.makedirs(os.path.dirname(custom_file)) except: pass cmd = "cp --force %s %s" % (original_file,custom_file) print(cmd) os.system(cmd) print("* Extract patch file (via 'git diff')") clean_file = custom_file + '.clean' patch_file = custom_file + '.patch' ret,clean_file,patch_file = extract_custom_patch(original_file,clean_file,patch_file,label,'',options) if 0 != ret: return(ret) print("* Copy cleaned version back to in-place location") cmd = "cp --force %s %s" % (clean_file,original_file) print(cmd) ret = os.system(cmd) return(ret) ################################# # patch_inplace # # Re-apply the custom patches on top of the general file, so that general fixes can be adopted # from upstream together with the customized patches applied on top # def patch_inplace(custom_file,original_file,patch_file,label,patcher_dir,options): # Skip if not in-place if not INPLACE_TAG in options: return 0 ret = 0 patch_file = custom_file + '.patch' if not os.path.isfile(patch_file): # No patch file found, do a simple copy-over of custom file if not os.path.isfile(custom_file): print("ERROR:SKIP: neither the patch file '%s' nor the custom file '%s' can be found" % (patch_file,custom_file)) return 1 print("* No patch file found, copying custom file over in-place file") cmd = "cp -f %s %s" % (custom_file,original_file) print(cmd) ret = os.system(cmd) else: print("* Patching the in-place file with custom patch") cmd = "patch %s %s" % (original_file,patch_file) print(cmd) ret = os.system(cmd) if 0 != ret: print("* ERROR: Merge failed, restoring previous custom file") cmd = "cp %s %s" % (custom_saved,custom_file) print(cmd) os.system(cmd) return(ret) ################################# # load_json_list # def load_json_list(json_file): label = 'CUSTOM' patcher_dir = '' file_list = [] with open(json_file) as json_data: dct = json.load(json_data) if 'label' in dct: label = dct['label'] if 'patcher_dir' in dct: patcher_dir = dct['patcher_dir'] if 'patch_set' in dct: for patch in dct['patch_set']: if DISABLE_TAG in patch['options']: continue file_list.append([patch['custom'],patch['original'],patch['patch'],patch['options']]) return file_list,patcher_dir,label ################################# # main loop # def main(argv): global verbose # setup parser = argparse.ArgumentParser(description='srtool_sanity_test.py: SRTool common sanity tests') parser.add_argument('--merge-original', '-o', action='store_const', const='merge_original', dest='command', help='Copy the (updated) original file, merge the custom patches') parser.add_argument('--merge-custom', '-c', action='store_const', const='merge_custom', dest='command', help='Copy the (updated) file to the original, without custom patches') parser.add_argument('--clean-inplace', '-i', action='store_const', const='clean_inplace', dest='command', help='Stash the customized file, leaving the clean mainline version inline') parser.add_argument('--patch-inplace', '-I', action='store_const', const='patch_inplace', dest='command', help='Restore the customized file in place of the mainline version inline') parser.add_argument('--json', '-J', help='Use JSON file for file list') parser.add_argument('--custom', '-C', help='Custom file') parser.add_argument('--original', '-O', help='Original file') parser.add_argument('--label', '-L', help='Custom label tag (default="CUSTOM")') parser.add_argument('--options', '-P', help='Options, for example "INPLACE", "DISABLE"') parser.add_argument('--extract-custom-patch', '-e', action='store_const', const='extract_custom_patch', dest='command', help='Extract a patch of the custom content') parser.add_argument('--patch', '-p', help='Patch file') parser.add_argument('--diff-original', '-d', action='store_const', const='diff_original', dest='command', help='Show how the file compares to original, ignoring custom patches') parser.add_argument('--verbose', '-v', action='store_true', dest='verbose', help='Debugging: verbose output') args = parser.parse_args() def validate_file(filename,msg): if not (filename or os.path.isfile(filename)): print("ERROR: %s file not found '%s'" % (msg,filename)) exit(1) # Extract provided values verbose = args.verbose file_list = [] if args.json: validate_file(args.json,"JSON") file_list,patcher_dir,label = load_json_list(args.json) else: original_file = args.original if args.original else '' custom_file = args.custom if args.custom else '' patch_file = args.patch if args.patch else '' label = args.label if args.label else 'CUSTOM' options = args.options if args.options else '' file_list.append([custom_file,original_file,patch_file,options]) patcher_dir = os.path.join(os.path.dirname(custom_file),'patcher') ret = 0 for custom_file,original_file,patch_file,options in file_list: #print("PATCH_FILE:%s,%s,%s,%s,%s,%s" % (custom_file,original_file,patch_file,options,patcher_dir,options)) if 'merge_original' == args.command: validate_file(custom_file,'Custom') validate_file(original_file,'Original') ret = merge_original(custom_file,original_file,patch_file,label,patcher_dir,options) elif 'merge_custom' == args.command: validate_file(custom_file,'Custom') validate_file(original_file,'Original') ret = merge_custom(custom_file,original_file,patch_file,label,patcher_dir,options) elif 'clean_inplace' == args.command: validate_file(custom_file,'Custom') validate_file(original_file,'Original') ret = clean_inplace(custom_file,original_file,patch_file,label,patcher_dir,options) elif 'patch_inplace' == args.command: validate_file(custom_file,'Custom') validate_file(original_file,'Original') ret = patch_inplace(custom_file,original_file,patch_file,label,patcher_dir,options) elif 'extract_custom_patch' == args.command: validate_file(custom_file,'Custom') ret,clean_file,patch_file = extract_custom_patch(custom_file,'',patch_file,label,patcher_dir,options) elif 'diff_original' == args.command: validate_file(custom_file,'Custom') validate_file(original_file,'Original') ret = diff_original(custom_file,original_file,patch_file,label,patcher_dir,options) else: print("Command not found '%s'" % args.command) ret = 1 if 0 != ret: exit(ret) exit(ret) if __name__ == '__main__': srtool_basepath = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(sys.argv[0])))) main(sys.argv[1:])