#!/usr/bin/env python3 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/cfgs.conf" # ----------------------------------------- # color defintions # ----------------------------------------- class bcolors: HEADER = '\033[95m' OKBLUE = '\033[94m' OKGREEN = '\033[92m' WARNING = '\033[93m' FAIL = '\033[91m' ENDC = '\033[0m' BOLD = '\033[1m' UNDERLINE = '\033[4m' # ----------------------------------------- # UI helper functions # ----------------------------------------- def check_dependencies(): status = True try: subprocess.call(["git", "version"], stdout=subprocess.DEVNULL) except OSError: status = False print(bcolors.FAIL + "git cannot be found on your system!" + bcolors.ENDC) print("Install git: pacman -S git") return status def print_splash(): print(bcolors.HEADER + "---=== cfgs ===---" + bcolors.ENDC) print("Version: " + CONFIG_SYNC_VERSION) print() print(bcolors.HEADER + "---=== USAGE ===---" + bcolors.ENDC) print("cfgs init") print("cfgs add ") print("cfgs remove ") print("cfgs store") print("cfgs restore") sys.exit(1) def init_dialog(): config = get_config() print(bcolors.HEADER + "---=== Welcome to cfgs! ===---" + bcolors.ENDC) print() if config['DEFAULT']['INITIALIZED']: reinitialize = input(bcolors.WARNING + "WARNING: cfgs has already been initialized! Do you really want to change the configuration? [y/N]: " + bcolors.ENDC) if not reinitialize or reinitialize != "y": print("Skipping new initialization!") sys.exit(0) local_path = input(bcolors.OKBLUE + "Enter the local storage directory for configuration files [" + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "]: " + bcolors.ENDC) if not local_path: local_path = config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] = local_path remote_repo = input(bcolors.OKBLUE + "Enter the remote repository url [" + config['GIT']['REMOTE_REPOSITORY'] + "]: " + bcolors.ENDC) if not remote_repo: remote_repo = config['GIT']['REMOTE_REPOSITORY'] config['GIT']['REMOTE_REPOSITORY'] = remote_repo git_ssh_key = input(bcolors.OKBLUE + "Enter the path to the ssh key to use [" + config['GIT']['SSHKEY'] + "]: " + bcolors.ENDC) if not git_ssh_key: git_ssh_key = config['GIT']['SSHKEY'] config['GIT']['SSHKEY'] = git_ssh_key git_user = input(bcolors.OKBLUE + "Enter the git user name (displayname) [" + config['GIT']['USER'] + "]: " + bcolors.ENDC) if not git_user: git_user = config['GIT']['USER'] config['GIT']['USER'] = git_user git_email = input(bcolors.OKBLUE + "Enter the git user email address [" + config['GIT']['EMAIL'] + "]: " + bcolors.ENDC) if not git_email: git_email = config['GIT']['EMAIL'] config['GIT']['EMAIL'] = git_email config['DEFAULT']['INITIALIZED'] = "True" update_config(config) # ----------------------------------------- # local config helper functions # ----------------------------------------- def validate_or_create_config(config): if not os.path.exists(CONFIG_SYNC_FILE): print(bcolors.OKBLUE + "Creating default configuration file: " + CONFIG_SYNC_FILE + bcolors.ENDC) config['DEFAULT'] = {} config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] = "/opt/cfgs/storage" config['DEFAULT']['INITIALIZED'] = "" config['GIT'] = {} config['GIT']['REMOTE_REPOSITORY'] = "" config['GIT']['USER'] = "cfgs" config['GIT']['EMAIL'] = "cfgs@" + 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(bcolors.FAIL + "Unable to write config file! Error: " + e.strerror + bcolors.ENDC) sys.exit(1) except Exception as e: print(bcolors.FAIL + "Unable to write config file! Error: " + str(e) + bcolors.ENDC) sys.exit(1) def create_initialization_file(path): try: with open(path + "/cfgs.info", mode='w') as file: file.write('cfgs repo initialized: %s. host: %s\n' % ( datetime.datetime.now(), socket.gethostname() )) except PermissionError as e: print(bcolors.FAIL + "Unable to write initialization file! Error: " + e.strerror + bcolors.ENDC) sys.exit(1) except Exception as e: print(bcolors.FAIL + "Unable to write initialization file! Error: " + str(e) + bcolors.ENDC) sys.exit(1) try: open(path + "/cfgs.db", mode='a').close() except PermissionError as e: print(bcolors.FAIL + "Unable to write database file! Error: " + e.strerror + bcolors.ENDC) sys.exit(1) except Exception as e: print(bcolors.FAIL + "Unable to write database file! Error: " + str(e) + bcolors.ENDC) sys.exit(1) # ----------------------------------------- # local database helper functions # ----------------------------------------- def add_file_to_db(file_path): config = get_config() try: insert = True with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/cfgs.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'] + "/cfgs.db", mode='a') as file: file.write('%s\n' % (file_path)) except PermissionError as e: print(bcolors.FAIL + "Unable to write db file! Error: " + e.strerror + bcolors.ENDC) sys.exit(1) except Exception as e: print(bcolors.FAIL + "Unable to write db file! Error: " + str(e) + bcolors.ENDC) sys.exit(1) def delete_file_from_db(file_path): config = get_config() try: with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/cfgs.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(bcolors.FAIL + "Unable to write db file! Error: " + e.strerror + bcolors.ENDC) sys.exit(1) except Exception as e: print(bcolors.FAIL + "Unable to write db file! Error: " + str(e) + bcolors.ENDC) sys.exit(1) # ----------------------------------------- # git helper functions # ----------------------------------------- def git_setup_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(bcolors.FAIL + bcolors.BOLD + "FATAL: Git operation failed! Repository has to be recovered manually!" + bcolors.ENDC) print(bcolors.FAIL + "FATAL: Repository path: " + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + bcolors.ENDC) sys.exit(1) # ----------------------------------------- # main business logic # ----------------------------------------- def init_local_repo(): config = get_config() print("Setting up local repository.") if not os.path.exists(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']): print(bcolors.OKBLUE + "Creating new directory: " + config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + bcolors.ENDC) try: os.makedirs(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) except OSError as e: print(bcolors.FAIL + "Failed to create local repository directory: " + e.strerror + bcolors.ENDC) 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'])) git_setup_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'], "cfgs initialized")) git_check_status(git_pull(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'])) print(bcolors.OKGREEN + "cfgs initialization successfull!" + bcolors.ENDC) print(bcolors.OKBLUE + "You can now use cfgs to keep your config files up to date." + bcolors.ENDC) def add_file(filepath): config = get_config() abs_path = os.path.abspath(filepath) if not os.path.exists(abs_path): print(bcolors.WARNING + "Invalid file, skipping!" + bcolors.ENDC) sys.exit(1) else: print(bcolors.OKBLUE + "Adding '" + abs_path + "' to cfgs..." + bcolors.ENDC) local_path = config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + abs_path if os.path.exists(local_path): print(bcolors.WARNING + "File is already registered to cfgs, skipping!" + bcolors.ENDC) 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 + ".cfgs", mode='w') as file: file.write('%s;%s;%s;%s;%s;%s' % ( file_permissions, file_owner, file_group, file_owner_name, file_group_name, socket.gethostname() )) except PermissionError as e: print(bcolors.FAIL + "Unable to write stat file! Error: " + e.strerror + bcolors.ENDC) sys.exit(1) except Exception as e: print(bcolors.FAIL + "Unable to write stat file! Error: " + str(e) + bcolors.ENDC) 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(bcolors.OKGREEN + "File added to cfgs!" + bcolors.ENDC) print(bcolors.OKBLUE + "Use cfgs store to push the file to the remote repository!" + bcolors.ENDC) 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(bcolors.WARNING + "File not registered to cfgs, skipping!" + bcolors.ENDC) sys.exit(1) else: print(bcolors.OKBLUE + "Removing '" + abs_path + "' from cfgs..." + bcolors.ENDC) 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(bcolors.OKBLUE + "Do you really want to override the local file '" + abs_path + "'? [y/N]: " + bcolors.ENDC) if not confirmed or confirmed != "y": print(bcolors.WARNING + "Skipping removal process!" + bcolors.ENDC) 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 + ".cfgs") delete_file_from_db(abs_path) print(bcolors.OKGREEN + "File removed from cfgs!" + bcolors.ENDC) print(bcolors.OKBLUE + "Use cfgs store to push the file to the remote repository!" + bcolors.ENDC) def store(): config = get_config() git_setup_ssh_environment() git_push(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) print(bcolors.OKGREEN + "cfgs store successfull!" + bcolors.ENDC) print(bcolors.OKBLUE + "You can now restore the configuration on your other systems." + bcolors.ENDC) def update_local_metadata(): config = get_config() with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/cfgs.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(bcolors.WARNING + "Invalid file in database, skipping! (" + abs_path + ")" + bcolors.ENDC) else: with open(local_path + ".cfgs", "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() git_setup_ssh_environment() git_pull(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY']) update_local_metadata() confirmed = input(bcolors.WARNING + "Do you really want to restore files from the repository? Local files will be overwritten! [y/N]: " + bcolors.ENDC) if not confirmed or confirmed != "y": print(bcolors.WARNING + "Skipping restore process!" + bcolors.ENDC) sys.exit(0) else: with open(config['DEFAULT']['LOCAL_STORAGE_DIRECTORY'] + "/cfgs.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(bcolors.WARNING + "Invalid file, skipping!" + bcolors.ENDC) 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) print(bcolors.OKGREEN + "cfgs restore successfull!" + bcolors.ENDC) print(bcolors.OKBLUE + "Configuration files have been updated. A reboot might be required." + bcolors.ENDC) # ----------------------------------------- # main entry point # ----------------------------------------- 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)