From ccd3339d1bf66f0453072c8fce1a03142a0b01ab Mon Sep 17 00:00:00 2001 From: Orsiris de Jong Date: Thu, 14 Dec 2023 18:15:37 +0100 Subject: [PATCH] WIP: Refactor config GUI --- npbackup/configuration.py | 41 +++++++-- npbackup/gui/config.py | 106 +++++++++++++++--------- npbackup/translations/config_gui.en.yml | 13 ++- npbackup/translations/config_gui.fr.yml | 13 ++- 4 files changed, 111 insertions(+), 62 deletions(-) diff --git a/npbackup/configuration.py b/npbackup/configuration.py index 2f29713..049aae2 100644 --- a/npbackup/configuration.py +++ b/npbackup/configuration.py @@ -96,13 +96,13 @@ empty_config_dict = { "repos": { "default": { "repo_uri": "", - "group": "default_group", + "repo_group": "default_group", "backup_opts": {}, "repo_opts": {}, "prometheus": {}, "env": { - "variables": {}, - "encrypted_variables": {} + "env_variables": [], + "encrypted_env_variables": [] }, }, }, @@ -167,8 +167,8 @@ empty_config_dict = { "group": "${MACHINE_GROUP}", }, "env": { - "variables": {}, - "encrypted_variables": {} + "env_variables": [], + "env_encrypted_variables": [] }, }, "identity": { @@ -361,6 +361,30 @@ def get_repo_config(full_config: dict, repo_name: str = 'default', eval_variable repo_config = ordereddict() config_inheritance = ordereddict() + def inherit_group_settings(repo_config: dict, group_config: dict) -> Tuple[dict, dict]: + """ + iter over group settings, update repo_config, and produce an identical version of repo_config + called config_inheritance, where every value is replaced with a boolean which states inheritance status + """ + + # WIP # TODO: cannot use local repo config, need to make a deepcopy first ? + + if isinstance(group_config, dict): + for key, value in group_config.items(): + if isinstance(value, dict): + repo_config[key] = inherit_group_settings(repo_config.g(key), group_config.g(key)) + elif isinstance(value, list) and isinstance(repo_config.g(key), list): + merged_lists = repo_config.g(key) + group_config.g(key) + repo_config.s(key, merged_lists) + config_inheritance.s(key, True) + else: + if not repo_config or not repo_config.g(key): + repo_config = group_config + config_inheritance.s(key, True) + else: + config_inheritance.s(key, False) + return repo_config, config_inheritance + try: repo_config = deepcopy(full_config.g(f'repos.{repo_name}')) # Let's make a copy of config since it's a "pointer object" @@ -369,10 +393,13 @@ def get_repo_config(full_config: dict, repo_name: str = 'default', eval_variable logger.error(f"No repo with name {repo_name} found in config") return None try: - repo_group = full_config.g(f'repos.{repo_name}.group') + repo_group = full_config.g(f'repos.{repo_name}.repo_group') + group_config = full_config.g(f'groups.{repo_group}') except KeyError: logger.warning(f"Repo {repo_name} has no group") else: + inherit_group_settings(repo_config, group_config) + """ sections = full_config.g(f'groups.{repo_group}') if sections: for section in sections: @@ -394,6 +421,7 @@ def get_repo_config(full_config: dict, repo_name: str = 'default', eval_variable config_inheritance.s(f'{section}.{entries}', True) else: config_inheritance.s(f'{section}.{entries}', False) + """ if eval_variables: repo_config = evaluate_variables(repo_config, full_config) @@ -469,7 +497,6 @@ def load_config(config_file: Path) -> Optional[dict]: if config_file_is_updated: logger.info("Updating config file") save_config(config_file, full_config) - return full_config diff --git a/npbackup/gui/config.py b/npbackup/gui/config.py index 2a03a99..8f17738 100644 --- a/npbackup/gui/config.py +++ b/npbackup/gui/config.py @@ -13,6 +13,7 @@ __build__ = "2023121401" from typing import List import os from logging import getLogger +from copy import deepcopy import PySimpleGUI as sg import npbackup.configuration as configuration from ofunctions.misc import get_key_from_value @@ -101,7 +102,7 @@ def config_gui(full_config: dict, config_file: str): return object_type, object_name - def update_gui_values(key, value, object_type, unencrypted): + def update_gui_values(key, value, inherited, object_type, unencrypted): """ Update gui values depending on their type """ @@ -112,12 +113,6 @@ def config_gui(full_config: dict, config_file: str): window[key].Disabled = True else: window[key].Disabled = False - # special case for env - if key == "env": - if isinstance(value, dict): - for envkey, envvalue in value.items(): - value = f"{envkey}={envvalue}" - try: # Don't show sensible info unless unencrypted requested if not unencrypted: @@ -141,27 +136,37 @@ def config_gui(full_config: dict, config_file: str): window[key].Update(combo_boxes[key][value]) else: window[key].Update(value) + + # Set inheritance on + inheritance_key = f'inherited.{key}' + if inheritance_key in window.AllKeysDict: + window[f'inherited.{key}'].update(visible=True if inherited else False) + except KeyError: logger.error(f"No GUI equivalent for key {key}.") except TypeError as exc: logger.error(f"Error: {exc} for key {key}.") - def iter_over_config(object_config: dict, object_type: str = None, unencrypted: bool = False, root_key: str =''): + def iter_over_config(object_config: dict, config_inheritance: dict = None, object_type: str = None, unencrypted: bool = False, root_key: str =''): """ Iter over a dict while retaining the full key path to current object """ base_object = object_config def _iter_over_config(object_config: dict, root_key=''): - # Special case where env is a dict but should be transformed into a list - if isinstance(object_config, dict) and not root_key == 'env': + # Special case where env is a dict but we should pass it directly as it to update_gui_values + if isinstance(object_config, dict): for key in object_config.keys(): if root_key: _iter_over_config(object_config[key], root_key=f'{root_key}.{key}') else: _iter_over_config(object_config[key], root_key=f'{key}') else: - update_gui_values(root_key, base_object.g(root_key), object_type, unencrypted) + if config_inheritance: + inherited = config_inheritance.g(root_key) + else: + inherited = False + update_gui_values(root_key, base_object.g(root_key), inherited, object_type, unencrypted) _iter_over_config(object_config, root_key) def update_object_gui(object_name = None, unencrypted=False): @@ -173,7 +178,6 @@ def config_gui(full_config: dict, config_file: str): for key in window.AllKeysDict: # We only clear config keys, wihch have '.' separator if "." in str(key): - print(key) window[key]('') object_type, object_name = get_object_from_combo(object_name) @@ -184,7 +188,20 @@ def config_gui(full_config: dict, config_file: str): # Now let's iter over the whole config object and update keys accordingly - iter_over_config(object_config, object_type, unencrypted, None) + iter_over_config(object_config, config_inheritance, object_type, unencrypted, None) + + def update_global_gui(full_config, unencrypted=False): + # TODO + return + global_config = deepcopy(full_config) + + # Only update global options gui with identified global keys + for key in global_config.keys(): + if key not in ('identity', 'global_prometheus', 'global_options'): + global_config.pop(key) + print(global_config) + + iter_over_config(global_config, None, None, unencrypted, None) def update_config_dict(values, full_config): @@ -259,6 +276,7 @@ def config_gui(full_config: dict, config_file: str): ), size=(40, 2), ), + sg.Text("inherited", key="inherited.backup_opts.use_fs_snapshot", visible=False), sg.Checkbox("", key="backup_opts.use_fs_snapshot", size=(41, 1)), ], [ @@ -356,6 +374,14 @@ def config_gui(full_config: dict, config_file: str): ] repo_col = [ + [ + sg.Text(_t("config_gui.backup_repo_uri"), size=(40, 1)), + sg.Input(key="repo_uri", size=(50, 1)), + ], + [ + sg.Text(_t("config_gui.repo_group"), size=(40, 1)), + sg.Input(key="repo_group", size=(50, 1)), + ], [ sg.Text( "{}\n({})".format( @@ -365,10 +391,6 @@ def config_gui(full_config: dict, config_file: str): ), sg.Input(key="repo_opts.minimum_backup_age", size=(50, 1)), ], - [ - sg.Text(_t("config_gui.backup_repo_uri"), size=(40, 1)), - sg.Input(key="repo_uri", size=(50, 1)), - ], [ sg.Text(_t("config_gui.backup_repo_password"), size=(40, 1)), sg.Input(key="repo_opts.repo_password", size=(50, 1)), @@ -403,28 +425,28 @@ def config_gui(full_config: dict, config_file: str): ], [ sg.Text(_t("config_gui.keep"), size=(30, 1)), - sg.Text(_t("config_gui.hourly"), size=(10, 1)), - sg.Input(key="repo_opts.retention_strategy.hourly", size=(50, 1)) + sg.Input(key="repo_opts.retention_strategy.hourly", size=(3, 1)), + sg.Text(_t("config_gui.hourly"), size=(20, 1)), ], [ sg.Text(_t("config_gui.keep"), size=(30, 1)), - sg.Text(_t("config_gui.daily"), size=(10, 1)), - sg.Input(key="repo_opts.retention_strategy.daily", size=(50, 1)) + sg.Input(key="repo_opts.retention_strategy.daily", size=(3, 1)), + sg.Text(_t("config_gui.daily"), size=(20, 1)), ], [ sg.Text(_t("config_gui.keep"), size=(30, 1)), - sg.Text(_t("config_gui.weekly"), size=(10, 1)), - sg.Input(key="repo_opts.retention_strategy.weekly", size=(50, 1)) + sg.Input(key="repo_opts.retention_strategy.weekly", size=(3, 1)), + sg.Text(_t("config_gui.weekly"), size=(20, 1)), ], [ sg.Text(_t("config_gui.keep"), size=(30, 1)), - sg.Text(_t("config_gui.monthly"), size=(10, 1)), - sg.Input(key="repo_opts.retention_strategy.monthly", size=(50, 1)) + sg.Input(key="repo_opts.retention_strategy.monthly", size=(3, 1)), + sg.Text(_t("config_gui.monthly"), size=(20, 1)), ], [ sg.Text(_t("config_gui.keep"), size=(30, 1)), - sg.Text(_t("config_gui.yearly"), size=(10, 1)), - sg.Input(key="repo_opts.retention_strategy.yearly", size=(50, 1)) + sg.Input(key="repo_opts.retention_strategy.yearly", size=(3, 1)), + sg.Text(_t("config_gui.yearly"), size=(20, 1)), ], ] @@ -485,7 +507,7 @@ def config_gui(full_config: dict, config_file: str): ), size=(40, 3), ), - sg.Multiline(key="env.variables", size=(48, 5)), + sg.Multiline(key="env.env_variables", size=(48, 5)), ], [ sg.Text( @@ -496,7 +518,7 @@ def config_gui(full_config: dict, config_file: str): ), size=(40, 3), ), - sg.Multiline(key="env.encrypted_variables", size=(48, 5)), + sg.Multiline(key="env.encrypted_env_variables", size=(48, 5)), ], ] @@ -514,7 +536,7 @@ def config_gui(full_config: dict, config_file: str): [[sg.Column(backup_col, scrollable=True, vertical_scroll_only=True)]], font="helvetica 16", key="--tab-backup--", - element_justification="C", + element_justification="L", ) ], [ @@ -523,7 +545,7 @@ def config_gui(full_config: dict, config_file: str): repo_col, font="helvetica 16", key="--tab-repo--", - element_justification="C", + element_justification="L", ) ], [ @@ -532,7 +554,7 @@ def config_gui(full_config: dict, config_file: str): prometheus_col, font="helvetica 16", key="--tab-prometheus--", - element_justification="C", + element_justification="L", ) ], [ @@ -541,13 +563,13 @@ def config_gui(full_config: dict, config_file: str): env_col, font="helvetica 16", key="--tab-env--", - element_justification="C", + element_justification="L", ) ], ] _layout = [ - [sg.Column(object_selector, element_justification='C')], + [sg.Column(object_selector, element_justification='L')], [sg.TabGroup(tab_group_layout, enable_events=True, key="--object-tabgroup--")], ] return _layout @@ -666,7 +688,7 @@ def config_gui(full_config: dict, config_file: str): identity_col, font="helvetica 16", key="--tab-global-identification--", - element_justification="C", + element_justification="L", ) ], [ @@ -675,7 +697,7 @@ def config_gui(full_config: dict, config_file: str): prometheus_col, font="helvetica 16", key="--tab-global-prometheus--", - element_justification="C", + element_justification="L", ) ], [ @@ -684,7 +706,7 @@ def config_gui(full_config: dict, config_file: str): global_options_col, font="helvetica 16", key="--tab-global-options--", - element_justification="C", + element_justification="L", ) ], [ @@ -693,7 +715,7 @@ def config_gui(full_config: dict, config_file: str): scheduled_task_col, font="helvetica 16", key="--tab-global-scheduled_task--", - element_justification="C", + element_justification="L", ) ], ] @@ -704,11 +726,11 @@ def config_gui(full_config: dict, config_file: str): return _layout - def config_layout(object_type: str = 'repo') -> List[list]: + def config_layout() -> List[list]: buttons = [ [ - #sg.Text(" " * 135), + sg.Push(), sg.Button(_t("generic.accept"), key="accept"), sg.Button(_t("generic.cancel"), key="cancel"), ] @@ -721,7 +743,8 @@ def config_gui(full_config: dict, config_file: str): _global_layout = [ [sg.TabGroup(tab_group_layout, enable_events=True, key="--configtabgroup--")], - [sg.Column(buttons, element_justification="C")], + [sg.Column(buttons, element_justification="L")], + [sg.Button("trololo")] ] return _global_layout @@ -745,6 +768,7 @@ def config_gui(full_config: dict, config_file: str): # Update gui with first default object (repo or group) update_object_gui(get_objects()[0], unencrypted=False) + update_global_gui(full_config, unencrypted=False) while True: event, values = window.read() diff --git a/npbackup/translations/config_gui.en.yml b/npbackup/translations/config_gui.en.yml index 1bbd09f..f25c209 100644 --- a/npbackup/translations/config_gui.en.yml +++ b/npbackup/translations/config_gui.en.yml @@ -46,7 +46,7 @@ en: no_config_available: No configuration file found. Please use --config-file "path" to specify one or copy a config file next to the NPBackup binary create_new_config: Would you like to create a new configuration ? saved_initial_config: If you saved your configuration, you may now reload this program - bogus_config_file: Bogus configuration file %{config_file} found + bogus_config_file: Bogus configuration file found encrypted_environment_variables: Encrypted envrionment variables (ie TOKENS etc) environment_variables: Environment variables @@ -103,9 +103,8 @@ en: files_from_raw: Files from raw list keep: Keep - copies: copies - hourly: hourly - daily: daily - weekly: weekly - monthly: monthly - yearly: yearly \ No newline at end of file + hourly: hourly copies + daily: daily copies + weekly: weekly copies + monthly: monthly copies + yearly: yearly copies \ No newline at end of file diff --git a/npbackup/translations/config_gui.fr.yml b/npbackup/translations/config_gui.fr.yml index 8f1071f..59d9357 100644 --- a/npbackup/translations/config_gui.fr.yml +++ b/npbackup/translations/config_gui.fr.yml @@ -46,7 +46,7 @@ fr: no_config_available: Aucun fichier de configuration trouvé. Merci d'utiliser --config-file "chemin" pour spécifier un fichier, ou copier un fichier de configuration a côté du binaire NPBackup. create_new_config: Souhaitez-vous créer une nouvelle configuration ? saved_initial_config: Si vous avez enregistré une configuration, vous pouvez à présent recharger le programme. - bogus_config_file: Fichier de configuration %{config_file} érroné + bogus_config_file: Fichier de configuration érroné encrypted_environment_variables: Variables d'envrionnement chiffrées (ie TOKENS etc) environment_variables: Variables d'environnement @@ -103,9 +103,8 @@ fr: files_from_raw: Liste fichiers depuis un fichier "raw" keep: Garder - copies: copies - hourly: par heure - daily: journalières - weekly: hebdomadaires - monthly: mensuelles - yearly: annuelles \ No newline at end of file + hourly: copies horaires + daily: copies journalières + weekly: copies hebdomadaires + monthly: copies mensuelles + yearly: copies annuelles \ No newline at end of file