Sauvegarder les fichiers de configuration sur GitHub

Exemple de code Python pour valider les fichiers de configuration de serveur critiques dans un référentiel GitHub

C05348A3-9AB8-42C9-A6E0-81DB3AC59FEB
           

Cet exercice est un peu l'inverse de celui décrit dans Distributed Server Management with GitHub . Cette fois, nous souhaitons sauvegarder les fichiers de configuration de serveur critiques, tels que les fichiers HAproxy ou nginx, sur GitHub. La modification de ces fichiers se fait généralement par essais et erreurs... modifiez le fichier, vérifiez la syntaxe, redémarrez le processus et vérifiez que la modification a le comportement prévu. Cela se fait évidemment sur un système de développement, pas en production.

Bien que ce soit absolument une bonne idée de stocker ces fichiers dans GitHub, le flux de travail GitHub normal pour valider une modification du référentiel et l'extraire des serveurs ne s'applique pas vraiment. Ces fichiers de configuration sont également généralement spécifiques à l'environnement, vous ne pouvez pas simplement transférer le fichier de configuration DEV sur le serveur de production, il existe des noms DNS, différents paramètres de journalisation et bien d'autres choses qui les rendent différents. J'ai personnellement trouvé que le meilleur flux de travail à utiliser est presque l'inverse du flux de travail GitHub normal, apportez la modification au fichier, testez et assurez-vous qu'il fonctionne comme prévu, puis validez la nouvelle version du fichier sur GitHub, en tant que sauvegarde. La sauvegarde peut être utilisée au cas où le serveur doit être reconstruit, ou également comme option de sauvegarde, pour annuler la prochaine modification de la version de travail précédente si les choses tournent mal.

C'est le but fondamental de ce script Python, de valider ces fichiers sur GitHub lorsqu'ils ont changé. Le script peut également masquer des paramètres spécifiques tels que les mots de passe, que vous ne souhaitez bien sûr jamais stocker sur GitHub ou tout autre référentiel de code. Un autre avantage de ce script est qu'il peut télécharger des fichiers vers un seul référentiel GitHub, dans un répertoire nommé d'après le nom du serveur, au lieu d'exiger un référentiel séparé pour chaque serveur.

Le script utilise la librairie PyGitHub , donc commençons par l'installer, ainsi que la librairie Yaml.

pip3 install PyGithub

Obtenir un jeton d'API auprès de GitHub

Pour utiliser le script suivant, vous devez d'abord obtenir un jeton d'API auprès de GitHub. Dans votre compte GitHub, accédez à "Paramètres" et "Paramètres du développeur", puis cliquez sur "jeton d'accès personnel" et créez un nouveau jeton. Selon cet article , sélectionnez "repo" comme champ d'application.

Vous devez absolument protéger ce jeton d'accès personnel car il permet d'accéder à votre compte, pas à n'importe quel référentiel particulier. Pour cela, j'ai décidé d'utiliser la bibliothèque RSA-crypto pour le chiffrer. Le code suivant installera la bibliothèque, créera un nouveau jeu de clés RSA et chiffrera le jeton d'accès personnel dans un fichier de configuration en tant que valeur de l'option nommée ghe_token . Voir Public/Private Key RSA Encryption et Python Encryption pour plus de détails sur l'utilisation de cette bibliothèque et de l'outil de ligne de commande associé.

Nous allons d'abord créer un nouveau chiffrement, en supposant que vous n'en avez pas déjà un, puis définir la valeur du jeton d'API GitHub obtenu ci-dessus en tant qu'entrée (option) nommée ghe_token dans le fichier de configuration.

pip3 install rsa-crypto 
 
rsa_crypto create
Enter key password: 
Re-enter key password: 
Creating key...
Created password-protected private/public keys file /Users/me/rsa_key.bin
Use the "extract" keyword to create public and private key files.
 
rsa_crypto set -o ghe_token
Using key: /Users/me/rsa_key.bin
Opening encrypted key /Users/me/rsa_key.bin
Enter key password: 
Enter value: 
set section:  option:ghe_token
Updated /Users/me/.rsa_values.conf

Le script Python sera alors configuré pour lire et déchiffrer l'"option" de configuration ghe_token afin de récupérer et d'utiliser le jeton d'accès personnel. Si cela semble trop complexe, n'hésitez pas à trouver une solution différente, mais s'il vous plaît, n'intégrez jamais de jeton d'accès directement dans votre script, il y a de fortes chances qu'il se retrouve sur GitHub pour que quiconque puisse le voir et l'utiliser !

Le script Python

Le code source est également disponible sur GitHub.

Les variables de configuration sont assez simples, file_list contient la liste des fichiers à valider sur GitHub s'ils existent localement.

La variable ghe_repo doit pointer vers votre référentiel GitHub. Je vous suggère fortement d'utiliser un référentiel différent dans le but de sauvegarder les fichiers de configuration et un référentiel différent si vous utilisez le script Distributed Server Management with GitHub .

La variable ghe_branch vous permet de pointer vers une branche différente, selon la façon dont vous utilisez GitHub.

La variable remote_base_path existe uniquement à cause d'un bogue précédent dans la bibliothèque PyGitHub où le chemin distant devait commencer par un /, mais ce n'est plus le cas.

Le script créera un fichier journal et tentera également de déterminer le nom de l'utilisateur exécutant le script à inclure en tant que commentaire de validation.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
 
__author__ = "Christophe Gauge"
__version__ = "1.0.4"
 
'''
Backup HAproxy and nginx configuration files.
'''
 
 
# I M P O R T S ###############################################################
 
 
import github
import os
import sys
import time
import io
import logging
import traceback
import hashlib
import rsa_crypto
import argparse
import socket
import getpass
import base64
if (sys.version_info > (3, 0)):
    from urllib.parse import urljoin
else:
    from urlparse import urljoin
 
 
# G L O B A L S ###############################################################
 
file_list = ['/tmp/test.txt', '/etc/haproxy/haproxy.cfg', '/etc/nginx/nginx.conf']
 
# # Uncomment if you are using your own internal GitHub Repository
# ghe_organization = 'my_ghe_repo'
# ghe_hostname = mydomain.com
 
ghe_repo = 'Christophe-Gauge/GitHub'
remote_base_path = ''
 
ghe_branch = 'main'
 
logger = logging.getLogger()
logging.basicConfig(level=logging.INFO)
logger.info("Path:    %s" % (os.path.realpath(__file__)))
logger.info("Version:  %s" % (__version__))
 
args = argparse.Namespace(option='ghe_token')
ghe_token = rsa_crypto.decrypt_value(args)
 
 
# C O D E #####################################################################
 
 
def main():
    """Main function."""
    global logger
    global repo
    global args
 
    # Get the server name to use as a directory in GitHub
    server_name = socket.gethostname().split('.')[0].lower()
    # Get the username to log who made the change, nobody will be a Cron task or such
    try:
        user_name = getpass.getuser()
    except Exception as e:
        user_name = None
    if user_name is None:
        try:
            user_name = os.getlogin()
        except OSError as e:
            user_name = 'nobody'
        except Exception as e:
            user_name = 'unknown'
 
    gh = github.Github(login_or_token=ghe_token)
    repo = gh.get_repo(ghe_repo)
 
    # # Uncomment if you are using your own internal GitHub Repository
    # gh = github.Github(base_url=f"https://{ghe_hostname}/api/v3", login_or_token=ghe_token)
    # org = gh.get_organization(ghe_organization)
    # repo = org.get_repo(ghe_repo)
 
    for local_file_name in file_list:
        file_content = ''
        if os.path.exists(local_file_name):
            logger.info('File %s exists, processing.' % local_file_name)
            # Redacting HAproxy auth passwords, more may be needed for your use-case
            with io.open(local_file_name, "r", encoding="utf-8") as f:
                for line in f:
                    if 'auth' in line:
                        file_content += line[:line.find('auth')] + 'auth <REMOVED>\n'
                    else:
                        file_content += line
            # print(file_content)
 
            data = file_content.encode('utf-8', 'ignore')
            filesize = len(data)
            content = "blob " + str(filesize) + "\0" + data.decode('utf-8')
            encoded_content = content.encode('utf-8')
            localSHA = hashlib.sha1(encoded_content).hexdigest()
 
            remote_file_name = urljoin(remote_base_path, server_name + '/' + os.path.basename(local_file_name))
            logger.info(f"Saving local file {local_file_name} to remote GitHub repo {repo.full_name} file {remote_file_name}")
 
            try:
                remoteSHA = repo.get_contents(remote_file_name, ref=ghe_branch).sha
            except github.UnknownObjectException as e:
                logger.error(f"Remote file not found {remote_file_name}")
                remoteSHA = None
            except Exception as e:
                logger.error("Error {0}".format(str(e)))
                logger.error(traceback.format_exc())
                remoteSHA = None
 
            if remoteSHA == localSHA:
                logger.info('Remote file is present, hash is the same, NOT updating.')
                continue
            else:
                try:
                    if remoteSHA is None:
                        logger.info('Remote file file is NOT present, creating new file')
                        repo.create_file(remote_file_name, f"Updated by {user_name}", data, branch=ghe_branch)
                    else:
                        logger.info('Remote file file is present but hash has changed, updating file')
                        repo.update_file(remote_file_name, f"Updated by {user_name}", data, remoteSHA, branch=ghe_branch)
 
                except Exception as e:
                    logger.error("Error {0}".format(str(e)))
                    logger.error(traceback.format_exc())
                logger.info('Done updating GitHub')
 
        else:
            logger.warning('File does not exist %s' % local_file_name)
 
    logger.info('*** DONE ***')
    sys.exit(0)
 
###############################################################################
 
 
if __name__ == "__main__":
    main()
 
# E N D   O F   F I L E #######################################################
Commentaires publiés : 0