From b8a2e121d2e9ffa8bbf3bd3544b0c186039c6654 Mon Sep 17 00:00:00 2001 From: Vadim Troshchinskiy Date: Thu, 12 Sep 2024 08:46:12 +0200 Subject: [PATCH] Add installer --- installer/README.md | 63 +++++ installer/opengnsys_git_installer.html | 216 +++++++++++++++ installer/opengnsys_git_installer.py | 370 +++++++++++++++++++++++++ 3 files changed, 649 insertions(+) create mode 100644 installer/README.md create mode 100644 installer/opengnsys_git_installer.html create mode 100755 installer/opengnsys_git_installer.py diff --git a/installer/README.md b/installer/README.md new file mode 100644 index 0000000..60f87b1 --- /dev/null +++ b/installer/README.md @@ -0,0 +1,63 @@ +# Instalación de dependencias para python + +La conversion del código a Python 3 requiere actualmente los paquetes especificados en `requirements.txt` + +Para instalar dependencias de python se usa el modulo venv (https://docs.python.org/3/library/venv.html) que instala todas las dependencias en un entorno independiente del sistema. + + +# Instalación rápida + + +## Distribuciones antiguas (18.04) + +**Nota:** En 18.04, `uname` solo se encuentra en `/bin`, lo que causa un error inocuo en el log durante la creación de los repositorios: + + Failed checking if running in CYGWIN due to: FileNotFoundError(2, 'No such file or directory') + +Se arregla con el symlink incluido en las instrucciones mas abajo. + + + sudo apt install -y python3.8 python3.8-venv python3-venv libarchive-dev + sudo ln -sf /bin/uname /usr/bin/ + python3.8 -m venv venvog + . venvog/bin/activate + python3.8 -m pip install --upgrade pip + pip3 install -r requirements.txt + +Ejecutar con: + + python3.8 ./opengnsys_git_installer.py + +## Distribuciones nuevas (22.04) + + sudo apt install python3 python3-venv libarchive-dev + python3 -m venv venvog + . venvog/bin/activate + python3 -m pip install --upgrade pip + pip3 install -r requirements.txt + +## Agregar clave de SSH si es necesario + +El proceso falla si no hay clave de SSH en la imagen. Utilizar: + + /opt/opengnsys/bin/setsslkey + +para agregarla. + +# Ejecutar + +**Nota:** Preferiblemente ejecutar como `root`, ya que `sudo` borra los cambios a las variables de entorno realizadas por venv. El resultado probable es un error de falta de módulos de Python, o un fallo del programa por usar dependencias demasiado antiguas. + + # . venv/bin/activate + # ./opengnsys_git_installer.py + +# Documentación + +Se puede generar documentación de Python con una utilidad como pdoc3 (hay multiples alternativas posibles): + + # Instalar pdoc3 + pip install --user pdoc3 + + # Generar documentación + pdoc3 --force --html opengnsys_git_installer.py + diff --git a/installer/opengnsys_git_installer.html b/installer/opengnsys_git_installer.html new file mode 100644 index 0000000..3fd7018 --- /dev/null +++ b/installer/opengnsys_git_installer.html @@ -0,0 +1,216 @@ + + + + +Python: module opengnsys_git_installer + + + + + +
 
opengnsys_git_installer
index
/home/vadim/opengnsys/opengnsys/installer/opengnsys_git_installer.py
+

Script para la instalación del repositorio git

+

+ + + + + +
 
Modules
       
argparse
+git
+grp
+
libarchive
+logging
+os
+
pwd
+shutil
+subprocess
+
sys
+tempfile
+

+ + + + + +
 
Classes
       
+
builtins.Exception(builtins.BaseException) +
+
+
RequirementException +
+
+
builtins.object +
+
+
FakeTemporaryDirectory +
Oglive +
OpengnsysGitInstaller +
+
+
+

+ + + + + + + +
 
class FakeTemporaryDirectory(builtins.object)
   FakeTemporaryDirectory(dirname)

+Imitación de TemporaryDirectory para depuración
 
 Methods defined here:
+
__init__(self, dirname)
Initialize self.  See help(type(self)) for accurate signature.
+ +
__str__(self)
Return str(self).
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class Oglive(builtins.object)
   Interfaz a utilidad oglivecli

+Esto es probablemente temporal hasta que se haga una conversión de oglivecli
 
 Methods defined here:
+
__init__(self)
Initialize self.  See help(type(self)) for accurate signature.
+ +
get_clients(self)
Devuelve la lista de clientes en un dict
+ +
get_default(self)
Devuelve el cliente por defecto
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class OpengnsysGitInstaller(builtins.object)
   Instalador de OpenGnsys
 
 Methods defined here:
+
__init__(self)
Inicializar clase
+ +
install(self)
Instalar

+Ejecuta todo el proceso de instalación incluyendo:
+* Dependencias
+* Configuración de authorized_keys
+* Configuración de ssh
+* Creación de repositorio

+Raises:
+    RequirementException: No ejecutado por usuario root
+    RequirementException: No ejecutado en Debian o Ubuntu
+    RequirementException: Falta clave pública
+    RequirementException: Python < 3.8
+ +
set_basepath(self, value)
Establece ruta base
+ +
set_ignoresshkey(self, value)
Ignorar clave de ssh
+ +
set_testmode(self, value)
Establece el modo de prueba
+ +
set_usesshkey(self, value)
Usar clave de ssh
+ +
+Data descriptors defined here:
+
__dict__
+
dictionary for instance variables
+
+
__weakref__
+
list of weak references to the object
+
+

+ + + + + + + +
 
class RequirementException(builtins.Exception)
   RequirementException(message)

+Excepción que arrojamos cuando nos falta algún requisito
 
 
Method resolution order:
+
RequirementException
+
builtins.Exception
+
builtins.BaseException
+
builtins.object
+
+
+Methods defined here:
+
__init__(self, message)
Initialize self.  See help(type(self)) for accurate signature.
+ +
+Data descriptors defined here:
+
__weakref__
+
list of weak references to the object
+
+
+Static methods inherited from builtins.Exception:
+
__new__(*args, **kwargs) from builtins.type
Create and return a new object.  See help(type) for accurate signature.
+ +
+Methods inherited from builtins.BaseException:
+
__delattr__(self, name, /)
Implement delattr(self, name).
+ +
__getattribute__(self, name, /)
Return getattr(self, name).
+ +
__reduce__(...)
Helper for pickle.
+ +
__repr__(self, /)
Return repr(self).
+ +
__setattr__(self, name, value, /)
Implement setattr(self, name, value).
+ +
__setstate__(...)
+ +
__str__(self, /)
Return str(self).
+ +
add_note(...)
Exception.add_note(note) --
+add a note to the exception
+ +
with_traceback(...)
Exception.with_traceback(tb) --
+set self.__traceback__ to tb and return self.
+ +
+Data descriptors inherited from builtins.BaseException:
+
__cause__
+
exception cause
+
+
__context__
+
exception context
+
+
__dict__
+
+
__suppress_context__
+
+
__traceback__
+
+
args
+
+

+ + + + + +
 
Functions
       
show_error(*args)
+
+ \ No newline at end of file diff --git a/installer/opengnsys_git_installer.py b/installer/opengnsys_git_installer.py new file mode 100755 index 0000000..ac16962 --- /dev/null +++ b/installer/opengnsys_git_installer.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python3 +"""Script para la instalación del repositorio git""" + +import os +import shutil +import argparse +import tempfile +import logging +import subprocess +import sys +import pwd +import grp +from termcolor import colored, cprint +import git +import libarchive + + +def show_error(*args): + """ + Imprime un mensaje de error + + Args: + *args: Argumentos igual que a la función print + + Returns: + None + """ + cprint(*args, "red", attrs = ["bold"], file=sys.stderr) + +class RequirementException(Exception): + """Excepción que indica que nos falta algún requisito + + Attributes: + message (str): Mensaje de error mostrado al usuario + """ + + def __init__(self, message): + """Inicializar RequirementException. + + Args: + message (str): Mensaje de error mostrado al usuario + """ + super().__init__(message) + self.message = message + +class FakeTemporaryDirectory: + """Imitación de TemporaryDirectory para depuración""" + def __init__(self, dirname): + self.name = dirname + os.makedirs(dirname, exist_ok=True) + + def __str__(self): + return self.name + +class Oglive: + """Interfaz a utilidad oglivecli + + Esto es probablemente temporal hasta que se haga una conversión de oglivecli + """ + + def __init__(self): + self.__logger = logging.getLogger("Oglive") + self.binary = "/opt/opengnsys/bin/oglivecli" + self.__logger.debug("Inicializando") + + def _cmd(self, args): + cmd = [self.binary] + args + self.__logger.debug("comando: %s", cmd) + + proc = subprocess.run(cmd, shell=False, check=True, capture_output=True) + out_text = proc.stdout.decode('utf-8').strip() + self.__logger.debug("salida: %s", out_text) + return out_text + + def get_default(self): + """Devuelve el cliente por defecto""" + self.__logger.debug("get_default()") + return self._cmd(["get-default"]) + + def get_clients(self): + """Devuelve la lista de clientes en un dict""" + self.__logger.debug("get_clients()") + lines = self._cmd(["list"]).splitlines() + clients = {} + for line in lines: + (number, name) = line.split() + clients[number] = name + + self.__logger.debug("Clientes: %s", clients) + return clients + +class OpengnsysGitInstaller: + """Instalador de OpenGnsys""" + + def __init__(self): + """Inicializar clase""" + self.__logger = logging.getLogger("OpengnsysGitInstaller") + self.__logger.setLevel(logging.DEBUG) + self.__logger.debug("Inicializando") + self.testmode = False + self.base_path = "/opt/opengnsys" + self.git_basedir = "base.git" + self.ssh_user = "opengnsys" + self.ssh_group = "opengnsys" + + self.ssh_homedir = pwd.getpwnam(self.ssh_user).pw_dir + self.ssh_uid = pwd.getpwnam(self.ssh_user).pw_uid + self.ssh_gid = grp.getgrnam(self.ssh_group).gr_gid + self.temp_dir = None + + # Possible names for SSH key + self.key_paths = ["scripts/ssl/id_rsa.pub", "scripts/ssl/id_ed25519.pub", "scripts/ssl/id_ecdsa.pub", "scripts/ssl/id_ed25519_sk.pub", "scripts/ssl/id_ecdsa_sk.pub"] + self.key_paths_dict = {} + + for kp in self.key_paths: + self.key_paths_dict[kp] = 1 + + + self.oglive = Oglive() + + + def set_testmode(self, value): + """Establece el modo de prueba""" + self.testmode = value + + def set_ignoresshkey(self, value): + """Ignorar requisito de clave de ssh para el instalador""" + self.ignoresshkey = value + + def set_usesshkey(self, value): + """Usar clave de ssh especificada""" + self.usesshkey = value + + def set_basepath(self, value): + """Establece ruta base de OpenGnsys + Valor por defecto: /opt/opengnsys + """ + self.base_path = value + + def _get_tempdir(self): + """Obtiene el directorio temporal""" + if self.testmode: + dirname = "/tmp/ogtemp" + if os.path.exists(dirname): + shutil.rmtree(dirname) + + dir=FakeTemporaryDirectory(dirname) + self.__logger.debug("Modo de prueba, temp=/tmp/ogtemp") + return dir + else: + dir = tempfile.TemporaryDirectory() + self.__logger.debug("Temp = %s", dir) + return dir + + def _cleanup(self): + """Limpia el directorio temporal""" + if self.temp_dir: + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def _init_git_repo(self, reponame): + """Inicializa un repositorio Git""" + # Creamos repositorio + ogdir_images = os.path.join(self.base_path, "images") + self.__logger.info("Creando repositorio de GIT %s", reponame) + + os.makedirs(os.path.join(ogdir_images, self.git_basedir), exist_ok=True) + + repo_path=os.path.join(ogdir_images, reponame) + shutil.rmtree(repo_path, ignore_errors=True) + + + # Marcar como directorio seguro + # Nota: no usar GitPython. Config global falla, aunque hay indicaciones de que + # git.Repo(path=None) es valido. Posiblemente bug de GitPython. + + subprocess.run(["git", "config", "--global", "add" "safe.directory", repo_path]) + + self.__logger.debug("Inicializando repositorio: " + repo_path) + repo = git.Repo.init(repo_path, bare = True) + + self.__logger.info("Configurando repositorio de GIT") + repo.config_writer().set_value("user", "name", "OpenGnsys").release() + repo.config_writer().set_value("user", "email", "OpenGnsys@opengnsys.com").release() + + def _add_line_to_file(self, filename, new_line): + """Agrega una línea a un archivo""" + found = False + + self.__logger.debug("Agregando linea: %s a %s", new_line, filename) + with open(filename, "a+", encoding="utf-8") as f: + f.seek(0) + + for line in f: + if line.strip() == new_line.strip(): + found = True + + if not found: + self.__logger.debug("Agregando linea: %s", new_line) + f.write(new_line + "\n") + else: + self.__logger.debug("Linea ya presente") + + def _recursive_chown(self, path, ouid, ogid): + """Cambia el propietario y grupo de forma recursiva""" + for dirpath, _, filenames in os.walk(path): + os.chown(dirpath, uid=ouid, gid=ogid) + for filename in filenames: + os.chown(os.path.join(dirpath, filename), uid=ouid, gid=ogid) + + + def install(self): + """Instalar + + Ejecuta todo el proceso de instalación incluyendo: + * Dependencias + * Configuración de authorized_keys + * Configuración de ssh + * Creación de repositorio + + Raises: + RequirementException: No ejecutado por usuario root + RequirementException: No ejecutado en Debian o Ubuntu + RequirementException: Falta clave pública + RequirementException: Python < 3.8 + """ + self.__logger.info("install()") + + ogdir_images = os.path.join(self.base_path, "images") + ENGINECFG = os.path.join(self.base_path, "client/etc/engine.cfg") + + os.environ["PATH"] += os.pathsep + os.path.join(self.base_path, "bin") + tftp_dir = os.path.join(self.base_path, "tftpboot") + INITRD = "oginitrd.img" + self.temp_dir = self._get_tempdir() + SSHUSER = "opengnsys" + + + # Control básico de errores. + self.__logger.debug("Comprobando euid") + if os.geteuid() != 0: + raise RequirementException("Sólo ejecutable por root") + + if not os.path.exists("/etc/debian_version"): + raise RequirementException("Instalación sólo soportada en Debian y Ubuntu") + + + MIN_PYTHON = (3, 8) + if sys.version_info < MIN_PYTHON: + raise RequirementException(f"Python %s.%s mínimo requerido.\n" % MIN_PYTHON) + + + self.__logger.debug("Instalando dependencias") + subprocess.run(["apt-get", "install", "-y", "git"], check=True) + + + # Autenticación del usuario opengnsys con clave pública desde los ogLive + # Requiere que todos los ogLive tengan la misma clave publica (utilizar setsslkey) + + # Tomamos la clave publica del cliente por defecto + default_num = self.oglive.get_default() + default_client = self.oglive.get_clients()[default_num] + + + client_initrd_path = os.path.join(tftp_dir, default_client, INITRD) + self.__logger.debug("Ruta de initrd: %s", client_initrd_path) + # Si me salgo con error borro el directorio temporal + + + if not self.ignoresshkey: + public_key="" + if self.usesshkey: + with open(self.usesshkey, 'r') as f: + public_key = f.read().strip() + + else: + if os.path.isfile(client_initrd_path): + #os.makedirs(temp_dir, exist_ok=True) + os.chdir(self.temp_dir.name) + self.__logger.debug("Descomprimiendo %s", client_initrd_path) + public_key = None + with libarchive.file_reader(client_initrd_path) as initrd: + for file in initrd: + self.__logger.debug("Archivo: %s", file) + + if file.pathname in self.key_paths_dict: + data = bytearray() + for block in file.get_blocks(): + data = data + block + public_key = data.decode('utf-8').strip() + + break + else: + print(f"No se encuentra la imagen de initrd {client_initrd_path}") + exit(2) + + # Si la clave publica no existe me salgo con error + if not public_key: + raise RequirementException(f"No se encuentra clave pública dentro del ogLive en {self.temp_dir}, imagen {client_initrd_path}. Rutas buscadas: {self.key_paths}\n" + + "Los oglive deben tener la misma clave pública (utilizar setsslkey)") + + + ssh_dir = os.path.join(self.ssh_homedir, ".ssh") + authorized_keys_file = os.path.join(ssh_dir, "authorized_keys") + + self.__logger.debug("Configurando ssh: Agregando clave %s a %s", public_key, authorized_keys_file) + self.__logger.debug("Key: %s", public_key) + os.makedirs(ssh_dir, exist_ok=True) + self._add_line_to_file(authorized_keys_file, public_key) + + os.chmod(authorized_keys_file, 0o600) + os.chown(ssh_dir, uid=self.ssh_uid, gid=self.ssh_gid) + os.chown(authorized_keys_file, uid=self.ssh_uid, gid=self.ssh_gid) + + # Configuramos el servicio ssh para que permita el acceso con clave pública + self.__logger.info(" Configuramos el servicio ssh para que permita el acceso con clave pública.") + with open("/etc/ssh/sshd_config", "r") as f: + sshd_config = f.read() + sshd_config = sshd_config.replace("PubkeyAuthentication no", "PubkeyAuthentication yes") + with open("/etc/ssh/sshd_config", "w") as f: + f.write(sshd_config) + os.system("systemctl reload ssh") + + # Instalamos git + os.system("apt install git") + + # Para que el usuario sólo pueda usar git (no ssh) + SHELL = shutil.which("git-shell") + os.system(f"usermod -s {SHELL} opengnsys") + + # Creamos repositorios + self._init_git_repo('windows.git') + self._init_git_repo('linux.git') + self._init_git_repo('mac.git') + + # Damos permiso al usuario opengnsys + for DIR in ["base.git", "linux.git", "windows.git"]: #, "LinAcl", "WinAcl"]: + self._recursive_chown(os.path.join(ogdir_images, DIR), ouid=self.ssh_uid, ogid=self.ssh_gid) + + + +if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(name)20s - [%(levelname)5s] - %(message)s') + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + logger.info("Inicio del programa") + + parser = argparse.ArgumentParser( + prog="OpenGnsys Installer", + description="Script para la instalación del repositorio git", + ) + parser.add_argument('--testmode', action='store_true', help="Modo de prueba") + parser.add_argument('--ignoresshkey', action='store_true', help="Ignorar clave de SSH") + parser.add_argument('--usesshkey', type=str, help="Usar clave SSH especificada") + + args = parser.parse_args() + + installer = OpengnsysGitInstaller() + installer.set_testmode(args.testmode) + installer.set_ignoresshkey(args.ignoresshkey) + installer.set_usesshkey(args.usesshkey) + + logger.debug("Inicio de instalación") + + try: + installer.install() + except RequirementException as req: + show_error(f"Requisito para la instalación no satisfecho: {req.message}") + exit(1) + +