Administración de servidores distribuidos con GitHub

Ejemplo de código de Python para actualizar archivos desde un repositorio de GitHub

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

Usé este script para actualizar un conjunto de scripts de administración que se ejecutaban en numerosos servidores. Podría haber revisado los scripts con un "git pull", pero quería controles adicionales sobre qué script estaba instalado en qué servidor, así que escribí un script de Python para hacer eso.

Llamo a esto un enfoque distribuido o descentralizado porque, a diferencia de tener una autoridad central como un servidor maestro Salt, Chef o Puppet que envía nuevos scripts de administración a todos los servidores, permite que los servidores extraigan scripts actualizados de GitHub.

El script usa la biblioteca PyGitHub , así que comencemos instalando eso, así como la biblioteca Yaml.

pip3 install PyGithub
pip3 install pyaml

Obtención de un token API de GitHub

Para usar el siguiente script, primero deberá obtener un token de API de GitHub. En su cuenta de GitHub, vaya a "Configuración" y "Configuración del desarrollador", y luego haga clic en "token de acceso personal" y cree un nuevo token. No se necesita ningún permiso adicional por ahora.

Definitivamente necesita proteger ese token de acceso personal, ya que permite el acceso a su cuenta, no solo a cualquier repositorio en particular. Para esto, decidí usar la biblioteca RSA-crypto para encriptarlo. El siguiente código instalará la biblioteca, creará un nuevo conjunto de claves RSA y cifrará el token de acceso personal en un archivo de configuración como un valor de la opción denominada ghe_token . Consulte Cifrado RSA de clave pública/privada y Cifrado de Python para obtener detalles adicionales sobre el uso de esa biblioteca y la herramienta de línea de comandos asociada.

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.
 
touch ~/.rsa_values.conf
 
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

La secuencia de comandos de Python se configurará para leer y descifrar la "opción" de configuración ghe_token para recuperar y usar el token de acceso personal. Si esto parece demasiado complejo, siéntase libre de encontrar una solución diferente, pero nunca incruste ningún token de acceso directamente en su secuencia de comandos, ¡existe una alta probabilidad de que termine en GitHub para que cualquiera lo vea y lo use!

El guión de Python

Antes de poder usar este script, deberá cambiar algunos parámetros, cambiar ghe_repo para que apunte a su propio repositorio y local_base_path para que apunte al directorio local en su sistema donde desea que residan los scripts de administración del servidor.

El script asume que está utilizando el GitHub público, pero el código necesario si está utilizando un repositorio privado de GitHub Enterprise interno de su organización también se ha incluido y comentado.

Puede encontrar este script en un repositorio de GitHub .

El script primero recuperará un archivo de configuración update_scripts.yaml. Ese archivo contiene tres secciones:

  • update_always : contiene una lista de archivos que siempre deben extraerse de GitHub
  • update_if_present : contiene una lista de archivos que deben actualizarse desde GitHub solo si están presentes
  • eliminar : contiene una lista de archivos que deben eliminarse del servidor local

Ese archivo le permite controlar de forma centralizada qué secuencias de comandos desea implementar en todos sus servidores. Si necesita instalar un nuevo script, insértelo en GitHub y agregue una entrada en el archivo de configuración, y sus servidores descargarán los nuevos scripts cuando se ejecute este script. También puede "engañar" a sus sistemas para que descarguen scripts de la sección update_if_present simplemente creando el archivo con el comando táctil y luego ejecutando este script de actualización. El script también se actualizará si está incluido en la lista.

Las tres secciones de los archivos se procesarán, los archivos en las secciones update_always y update_if_present solo se actualizarán en el hash de GitHub que es diferente del hash local, lo que hace que la extracción sea más eficiente. El script también hará que todos los scripts de Python sean ejecutables para el usuario actual.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import (absolute_import, division,
                        print_function, unicode_literals)
 
__author__ = "Videre Research, LLC"
__version__ = "1.0.2"
 
 
'''
 
Install and configure the server management scripts and other supporting scripts.
 
'''
 
 
# I M P O R T S ###############################################################
 
import os
import sys
import logging
import traceback
import base64
import github
import yaml
import hashlib
import rsa_crypto
import argparse
 
if (sys.version_info > (3, 0)):
    from urllib.parse import urljoin
else:
    from urlparse import urljoin
 
 
# G L O B A L S ###############################################################
 
# # 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 = '/'
local_base_path = '/opt/scripts'
remote_config_file = 'update_scripts.yaml'
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 get_ghe(remote_file, repo, always_update):
    """Get a file from GHE and save it locally"""
    my_file_name = os.path.basename(remote_file)
    remote_file_name = urljoin(remote_base_path, remote_file)
    local_file_name = os.path.join(local_base_path, my_file_name)
    logger.info("Retrieving remote GHE file %s%s to %s" % (repo.full_name, remote_file_name, local_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}")
        return
    except Exception as e:
        logger.error("Error {0}".format(str(e)))
        logger.error(traceback.format_exc())
        return
 
    # If the file exists then let's get the hash to see if an update is needed
    if os.path.exists(local_file_name):
        # Compute the SHA1 hash of the local file
        with open(local_file_name, 'rb') as file_for_hash:
            data = file_for_hash.read()
        filesize = len(data)
        content = "blob " + str(filesize) + "\0" + data.decode('utf-8')
        encoded_content = content.encode('utf-8')
        localSHA = hashlib.sha1(encoded_content).hexdigest()
        if remoteSHA == localSHA:
            logger.info('File is present, hash is the same, we already have the latest file, NOT updating.')
            return
        else:
            logger.info('File is present, hash is different %s - %s' % (remoteSHA, localSHA))
    else:
        # This flag indicates that a file should only be updated if it already exists
        if not always_update:
            logger.info('File is not present NOT updating')
            return
    try:
        file_contents = repo.get_contents(remote_file_name, ref=ghe_branch)
        local_file_content = str(base64.b64decode(file_contents.content).decode('utf-8', 'ignore'))
 
        # Write the new file to disk
        with open(local_file_name, "w") as text_file:
            text_file.write(local_file_content)
        if my_file_name.endswith('.py'):
            os.chmod(local_file_name, 0o700)
        else:
            os.chmod(local_file_name, 0o400)
        logger.info('File was updated')
 
    except Exception as e:
        logger.error("Error {0}".format(str(e)))
        logger.error(traceback.format_exc())
 
def main():
    """Main function."""
 
    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)
 
    if not os.path.exists(local_base_path):
        os.makedirs(local_base_path)
 
    remote_file_name = urljoin(remote_base_path, remote_config_file)
    logger.info("Retrieving remote GHE file %s%s" % (repo.full_name, remote_file_name))
 
    try:
        file_contents = repo.get_contents(remote_file_name, ref=ghe_branch)
        text_contents = str(base64.b64decode(file_contents.content).decode('utf-8', 'ignore'))
        file_list = yaml.load(text_contents, Loader=yaml.SafeLoader)
        logger.info(yaml.safe_dump(file_list, default_flow_style=False))
    except Exception as e:
        if e.args[0] == 404:
            logger.error(f"Remote file not found {remote_file_name}")
            sys.exit(1)
        else:
            logger.error("Error {0}".format(str(e)))
            logger.error(traceback.format_exc())
            sys.exit(1)
 
    for file in file_list['update_always']:
        logger.info(file)
        get_ghe(file, repo, True)
 
    for file in file_list['update_if_present']:
        logger.info(file)
        get_ghe(file, repo, False)
 
    for file in file_list['remove']:
        if os.path.exists(file):
            os.remove(file)
            logger.info('File %s was deleted' % file)
 
    sys.exit(0)
 
###############################################################################
 
 
if __name__ == "__main__":
    main()
 
# E N D   O F   F I L E #######################################################
Comentarios publicados: 0

Tagged with:
GitHub