Example #1
0
 def test_summarize_compare_results(self):
     c1 = parse_string(self.cfg_props_imapsync_1)
     c2 = parse_string(self.cfg_props_imapsync_2)
     diffs = compare_cfgs(c1, c2)
     output = StringIO()
     summarize_cfg_diffs(diffs, output)
     out = output.getvalue()
     # Very basic check for now.
     self.assertRegex(out, r"\[imapsync\]\s*3 keys")
     self.assertRegex(out, r"\[other2\]")
Example #2
0
 def test_summarize_compare_results(self):
     c1 = parse_string(self.cfg_props_imapsync_1)
     c2 = parse_string(self.cfg_props_imapsync_2)
     diffs = compare_cfgs(c1, c2)
     output = StringIO()
     summarize_cfg_diffs(diffs, output)
Example #3
0
    def run(self, args):
        if isinstance(args.target, ConfDirProxy):
            # If a directory is given instead of a target file, then assume the source filename
            # and target filename are the same.
            # Also handle local/default meta:     e.g.:   ksconf promote local.meta .
            source_basename = os.path.basename(args.source.name)
            if source_basename == "local.meta":
                args.target = args.target.get_file("default.meta")
            else:
                args.target = args.target.get_file(source_basename)
            del source_basename

        if not os.path.isfile(args.target.name):
            self.stdout.write(
                "Target file {} does not exist.  Moving source file {} to the target."
                .format(args.target.name, args.source.name))
            # For windows:  Close out any open file descriptors first
            args.target.close()
            args.source.close()
            if args.keep:
                shutil.copy2(args.source.name, args.target.name)
            else:
                shutil.move(args.source.name, args.target.name)
            return

        # If src/dest are the same, then the file ends up being deleted.  Whoops!
        if _samefile(args.source.name, args.target.name):
            self.stderr.write(
                "Aborting.  SOURCE and TARGET are the same file!\n")
            return EXIT_CODE_FAILED_SAFETY_CHECK

        fp_source = file_fingerprint(args.source.name)
        fp_target = file_fingerprint(args.target.name)

        # Todo: Add a safety check prevent accidental merge of unrelated files.
        # Scenario: promote local/props.conf into default/transforms.conf
        # Possible check (1) Are basenames are different?  (props.conf vs transforms.conf)
        # Possible check (2) Are there key's in common? (DEST_KEY vs REPORT)
        # Using #1 for now, consider if there's value in #2
        bn_source = os.path.basename(args.source.name)
        bn_target = os.path.basename(args.target.name)
        if bn_source.endswith(".meta") and bn_target.endswith(".meta"):
            # Allow local.meta -> default.meta without --force or a warning message
            pass
        elif bn_source != bn_target:
            # Todo: Allow for interactive prompting when in interactive but not force mode.
            if args.force:
                self.stderr.write(
                    "Promoting content across conf file types ({0} --> {1}) because the "
                    "'--force' CLI option was set.\n".format(
                        bn_source, bn_target))
            else:
                self.stderr.write(
                    "Refusing to promote content between different types of configuration "
                    "files.  {0} --> {1}  If this is intentional, override this safety"
                    "check with '--force'\n".format(bn_source, bn_target))
                return EXIT_CODE_FAILED_SAFETY_CHECK

        # Todo:  Preserve comments in the TARGET file.  Worry with promoting of comments later...
        # Parse all config files
        cfg_src = args.source.data
        cfg_tgt = args.target.data

        if not cfg_src:
            self.stderr.write(
                "No settings in {}.  Nothing to promote.\n".format(
                    args.source.name))
            return EXIT_CODE_NOTHING_TO_DO

        if args.mode == "ask":
            # Show a summary of how many new stanzas would be copied across; how many key changes.
            # And either accept all (batch) or pick selectively (batch)
            delta = compare_cfgs(cfg_tgt, cfg_src, allow_level0=False)
            delta = [op for op in delta if op.tag != DIFF_OP_DELETE]
            summarize_cfg_diffs(delta, self.stderr)

            while True:
                resp = input("Would you like to apply ALL changes?  (y/n/d/q)")
                resp = resp[:1].lower()
                if resp == 'q':
                    return EXIT_CODE_USER_QUIT
                elif resp == 'd':
                    show_diff(self.stdout,
                              delta,
                              headers=(args.source.name, args.target.name))
                elif resp == 'y':
                    args.mode = "batch"
                    break
                elif resp == 'n':
                    args.mode = "interactive"
                    break

        if args.mode == "interactive":
            (cfg_final_src, cfg_final_tgt) = self._do_promote_interactive(
                cfg_src, cfg_tgt, args)
        else:
            (cfg_final_src, cfg_final_tgt) = self._do_promote_automatic(
                cfg_src, cfg_tgt, args)

        # Minimize race condition:  Do file mtime/hash check here.  Abort on external change.
        # Todo: Eventually use temporary files and atomic renames to further minimize the risk
        # Todo: Make backup '.bak' files (user configurable)
        # Todo: Avoid rewriting files if NO changes were made. (preserve prior backups)
        # Todo: Restore file modes and such

        if file_fingerprint(args.source.name, fp_source):
            self.stderr.write(
                "Aborting!  External source file changed: {0}\n".format(
                    args.source.name))
            return EXIT_CODE_EXTERNAL_FILE_EDIT
        if file_fingerprint(args.target.name, fp_target):
            self.stderr.write(
                "Aborting!  External target file changed: {0}\n".format(
                    args.target.name))
            return EXIT_CODE_EXTERNAL_FILE_EDIT
        # Reminder:  conf entries are being removed from source and promoted into target
        args.target.dump(cfg_final_tgt)
        if not args.keep:
            # If --keep is set, we never touch the source file.
            if cfg_final_src:
                args.source.dump(cfg_final_src)
            else:
                # Config file is empty.  Should we write an empty file, or remove it?
                if args.keep_empty:
                    args.source.dump(cfg_final_src)
                else:
                    args.source.unlink()