From f793ba7bb64b0ebc40f13290ada285883ad40d9f Mon Sep 17 00:00:00 2001 From: Christoph Haas Date: Wed, 5 Sep 2018 22:03:16 +0200 Subject: [PATCH] First functional release --- configsync.py | 421 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 421 insertions(+) create mode 100644 configsync.py diff --git a/configsync.py b/configsync.py new file mode 100644 index 0000000..830db69 --- /dev/null +++ b/configsync.py @@ -0,0 +1,421 @@ +import os +import sys +import subprocess +import configparser +import socket +import datetime +import shutil +from pathlib import Path +import stat +import pwd +import grp + +CONFIG_SYNC_VERSION = "1.0.0" +CONFIG_SYNC_FILE = "/etc/configsync.conf" + +def check_dependencies(): + status = True + try: + subprocess.call(["git", "version"], stdout=subprocess.DEVNULL) + except OSError: + status = False + print("git cannot be found on your system!") + print("Install git: pacman -S git") + + return status + +def print_splash(): + print("---=== CONFIG-SYNC ===---") + print("Version: " + CONFIG_SYNC_VERSION) + print() + print("---=== USAGE ===---") + print("configsync init") + print("configsync add ") + print("configsync remove ") + print("configsync store") + print("configsync restore") + sys.exit(1) + +def validate_or_create_config(config): + if not os.path.exists(CONFIG_SYNC_FILE): + print("Creating default configuration file: " + CONFIG_SYNC_FILE) + config['DEFAULT'] = {} + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] = "/opt/configsync/storage" + config['DEFAULT']['INITIALIZED'] = "" + config['GIT'] = {} + config['GIT']['REMOTE_REPOSITORY'] = "" + config['GIT']['USER'] = "configsync" + config['GIT']['EMAIL'] = "configsync@" + socket.gethostname() + config['GIT']['SSHKEY'] = str(Path.home()) + "/.ssh/id_rsa" + + update_config(config) + else: + config.read(CONFIG_SYNC_FILE) + + return config + +def get_config(): + config = configparser.ConfigParser() + config = validate_or_create_config(config) + + return config + +def update_config(config): + try: + config.write(open(CONFIG_SYNC_FILE, 'w')) + except PermissionError as e: + print("Unable to write config file! Error: " + e.strerror) + sys.exit(1) + except Exception as e: + print("Unable to write config file! Error: " + str(e)) + sys.exit(1) + +def init_dialog(): + config = get_config() + print("Welcome to CONFIG-SYNC!") + print() + + if config['DEFAULT']['INITIALIZED']: + reinitialize = input("WARNING: CONFIG-SYNC has already been initialized! Do you really want to change the configuration? [y/N]: ") + if not reinitialize or reinitialize != "y": + print("Skipping new initialization!") + sys.exit(0) + + local_path = input("Enter the local storage directory for configuration files [" + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "]: ") + if not local_path: + local_path = config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] = local_path + + remote_repo = input("Enter the remote repository url [" + config['GIT']['REMOTE_REPOSITORY'] + "]: ") + if not remote_repo: + remote_repo = config['GIT']['REMOTE_REPOSITORY'] + config['GIT']['REMOTE_REPOSITORY'] = remote_repo + + git_ssh_key = input("Enter the path to the ssh key to use [" + config['GIT']['SSHKEY'] + "]: ") + if not git_ssh_key: + git_ssh_key = config['GIT']['SSHKEY'] + config['GIT']['SSHKEY'] = git_ssh_key + + git_user = input("Enter the git user name (displayname) [" + config['GIT']['USER'] + "]: ") + if not git_user: + git_user = config['GIT']['USER'] + config['GIT']['USER'] = git_user + + git_email = input("Enter the git user email address [" + config['GIT']['EMAIL'] + "]: ") + if not git_email: + git_email = config['GIT']['EMAIL'] + config['GIT']['EMAIL'] = git_email + + config['DEFAULT']['INITIALIZED'] = "True" + + update_config(config) + +def create_initialization_file(path): + try: + with open(path + "/configsync.info", mode='w') as file: + file.write('CONFIG-SYNC Repo initialized: %s.\n' % ( datetime.datetime.now())) + except PermissionError as e: + print("Unable to write initialization file! Error: " + e.strerror) + sys.exit(1) + except Exception as e: + print("Unable to write initialization file! Error: " + str(e)) + sys.exit(1) + + try: + open(path + "/configsync.db", mode='a').close() + except PermissionError as e: + print("Unable to write database file! Error: " + e.strerror) + sys.exit(1) + except Exception as e: + print("Unable to write database file! Error: " + str(e)) + sys.exit(1) + +def add_file_to_db(file_path): + config = get_config() + try: + insert = True + with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/configsync.db", "r+") as search: + for line in search: + line = line.rstrip() # remove '\n' at end of line + if file_path == line: + insert = False + if insert: + with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/configsync.db", mode='a') as file: + file.write('%s\n' % (file_path)) + except PermissionError as e: + print("Unable to write db file! Error: " + e.strerror) + sys.exit(1) + except Exception as e: + print("Unable to write db file! Error: " + str(e)) + sys.exit(1) + +def delete_file_from_db(file_path): + config = get_config() + try: + with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/configsync.db", "r+") as search: + lines = search.readlines() + search.seek(0) + for line in lines: + if line.rstrip() != file_path: + search.write(line.rstrip() + "\n") + search.truncate() + except PermissionError as e: + print("Unable to write db file! Error: " + e.strerror) + sys.exit(1) + except Exception as e: + print("Unable to write db file! Error: " + str(e)) + sys.exit(1) + +def setup_git_ssh_environment(): + config = get_config() + + os.environ['GIT_SSH_COMMAND'] = "ssh -i " + config['GIT']['SSHKEY'] + +def git_is_repo(path): + return subprocess.call(["git", "branch"], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, cwd=path) == 0 + +def git_set_author(path, user, email): + status = subprocess.call(["git", "config", "user.name", user], stderr=subprocess.STDOUT, stdout=subprocess.DEVNULL, cwd=path) == 0 + status |= subprocess.call(["git", "config", "user.email", email], stderr=subprocess.STDOUT, stdout=subprocess.DEVNULL, cwd=path) == 0 + + return status + +def git_set_upstream(path, remote_url): + # make sure that remote origin does not exist + status = subprocess.call(["git", "remote", "rm", "origin"], stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL, cwd=path) == 0 + # now add new origin + status |= subprocess.call(["git", "remote", "add", "origin", remote_url], cwd=path) == 0 + + return status + +def git_init_repo(path): + status = subprocess.call(["git", "init"], cwd=path) == 0 + + return status + +def git_push(path, message = "configuration update"): + status = subprocess.call(["git", "add", "-A"], cwd=path) == 0 + status |= subprocess.call(["git", "commit", "-a", "-m", message], cwd=path) == 0 + status |= subprocess.call(["git", "push", "-u", "origin", "master"], cwd=path) == 0 + + return status + +def git_pull(path): + status = subprocess.call(["git", "pull", "origin", "master"], cwd=path) == 0 + + return status + +def git_check_status(status): + config = get_config() + if not status: + print("FATAL: Git operation failed! Repository has to be recovered manually!") + print("FATAL: Repository path: " + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) + sys.exit(1) + +def init_local_repo(): + config = get_config() + + print("Setting up local repository.") + if not os.path.exists(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']): + print("Creating new directory: " + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) + try: + os.makedirs(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) + except OSError as e: + print("Failed to create local repository directory: " + e.strerror) + sys.exit(1) + + # init git repo + if not git_is_repo(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']): + git_init_repo(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) + + git_check_status(git_set_author(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'], config['GIT']['USER'], config['GIT']['EMAIL'])) + git_check_status(git_set_upstream(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'], config['GIT']['REMOTE_REPOSITORY'])) + + setup_git_ssh_environment() + + git_check_status(git_pull(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'])) + create_initialization_file(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) + git_check_status(git_push(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'], "configsync initialized")) + git_check_status(git_pull(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'])) + +def add_file(filepath): + config = get_config() + abs_path = os.path.abspath(filepath) + + if not os.path.exists(abs_path): + print("Invalid file, skipping!") + sys.exit(1) + else: + print("Adding '" + abs_path + "' to CONFIG-SYNC...") + + local_path = config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + abs_path + + if os.path.exists(local_path): + print("File is already registered to CONFIG-SYNC, skipping!") + sys.exit(1) + else: + target_directory = os.path.abspath(os.path.join(local_path, os.pardir)) + if not os.path.exists(target_directory): + os.makedirs(target_directory) + + file_stat = os.stat(abs_path) + file_permissions = file_stat[stat.ST_MODE] + file_owner = file_stat[stat.ST_UID] + file_group = file_stat[stat.ST_GID] + file_owner_name = pwd.getpwuid(file_owner).pw_name + file_group_name = grp.getgrgid(file_group).gr_name + + try: + with open(local_path + ".cfsinfo", mode='w') as file: + file.write('%s;%s;%s;%s;%s' % (file_permissions, file_owner, file_group, file_owner_name, file_group_name)) + except PermissionError as e: + print("Unable to write stat file! Error: " + e.strerror) + sys.exit(1) + except Exception as e: + print("Unable to write stat file! Error: " + str(e)) + sys.exit(1) + + shutil.copy2(abs_path, local_path) + os.chown(local_path, file_owner, file_group) + os.remove(abs_path) + os.symlink(local_path, abs_path) + os.chown(abs_path, file_owner, file_group, follow_symlinks=False) + + add_file_to_db(abs_path) + + print("File added to CONFIG-SYNC!") + print("Use configsync store to push the file to the remote repository!") + +def remove_file(filepath): + config = get_config() + abs_path = os.path.abspath(filepath) + + local_path = config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + abs_path + + if not os.path.exists(local_path): + print("File not registered to CONFIG-SYNC, skipping!") + sys.exit(1) + else: + print("Removing '" + abs_path + "' from CONFIG-SYNC...") + + target_directory = os.path.abspath(os.path.join(abs_path, os.pardir)) + if not os.path.exists(target_directory): + os.makedirs(target_directory) + + if os.path.exists(abs_path): + confirmed = input("Do you really want to override the local file '" + abs_path + "'? [y/N]: ") + if not confirmed or confirmed != "y": + print("Skipping removal process!") + sys.exit(0) + else: + os.remove(abs_path) + + shutil.copy2(local_path, abs_path) + + file_stat = os.stat(local_path) + file_permissions = file_stat[stat.ST_MODE] + file_owner = file_stat[stat.ST_UID] + file_group = file_stat[stat.ST_GID] + + os.chown(abs_path, file_owner, file_group) + os.chmod(abs_path, file_permissions) + os.remove(local_path) + os.remove(local_path + ".cfsinfo") + + delete_file_from_db(abs_path) + + print("File removed from CONFIG-SYNC!") + print("Use configsync store to push the file to the remote repository!") + +def store(): + config = get_config() + setup_git_ssh_environment() + git_push(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) + +def update_local_metadata(): + config = get_config() + with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/configsync.db", "r") as search: + for line in search: + abs_path = line.rstrip() # remove '\n' at end of line + local_path = config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + abs_path + + if not os.path.exists(local_path): + print("Invalid file in database, skipping!") + else: + with open(local_path + ".cfsinfo", "r") as metadata_file: + metadata_string = metadata_file.readline().rstrip() + metadata = metadata_string.split(";") + + file_permissions = int(metadata[0]) + file_owner = int(metadata[1]) + file_group = int(metadata[2]) + + os.chmod(local_path, file_permissions) + os.chown(local_path, file_owner, file_group) + +def restore(): + config = get_config() + setup_git_ssh_environment() + git_pull(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) + + update_local_metadata() + + confirmed = input("Do you really want to restore files from the repository? Local files will be overwritten! [y/N]: ") + if not confirmed or confirmed != "y": + print("Skipping restore process!") + sys.exit(0) + else: + with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/configsync.db", "r") as search: + for line in search: + abs_path = line.rstrip() # remove '\n' at end of line + local_path = config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + abs_path + + if not os.path.exists(local_path): + print("Invalid file, skipping!") + else: + target_directory = os.path.abspath(os.path.join(abs_path, os.pardir)) + if not os.path.exists(target_directory): + os.makedirs(target_directory) + + if os.path.exists(abs_path): + os.remove(abs_path) + + os.symlink(local_path, abs_path) + + file_stat = os.stat(local_path) + file_owner = file_stat[stat.ST_UID] + file_group = file_stat[stat.ST_GID] + + os.chown(abs_path, file_owner, file_group, follow_symlinks=False) + +def main(argv): + if not check_dependencies(): + sys.exit(1) + + if len(argv) == 2: + arg = argv[1] + if arg == "init": + init_dialog() + init_local_repo() + sys.exit(0) + elif arg == "store": + store() + elif arg == "restore": + restore() + else: + print_splash() + elif len(argv) == 3: + arg = argv[1] + config_file = argv[2] + + if arg == "add": + add_file(config_file) + elif arg == "remove": + remove_file(config_file) + else: + print_splash() + else: + print_splash() + +if __name__ == "__main__": + main(sys.argv) \ No newline at end of file