Compare commits
No commits in common. "main" and "ogrepository-integration" have entirely different histories.
main
...
ogreposito
|
@ -6,5 +6,3 @@ venvog
|
|||
*.dsc
|
||||
*.changes
|
||||
*.buildinfo
|
||||
*.tar.gz
|
||||
*-stamp
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
# GitLib
|
||||
|
||||
The `gitapi.py` is an API for OgGit, written in Python/Flask.
|
||||
|
||||
It is an HTTP server that receives commands and executes maintenance actions including the creation and deletion of repositories.
|
||||
|
||||
|
||||
# Installing Python dependencies
|
||||
|
||||
The conversion of the code to Python 3 currently requires the packages specified in `requirements.txt`.
|
||||
|
||||
To install Python dependencies, the `venv` module is used (https://docs.python.org/3/library/venv.html), which installs all dependencies in an environment independent of the system.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
|
||||
# Ubuntu 24.04
|
||||
|
||||
sudo apt install -y python3-flask python3-paramiko opengnsys-flask-executor opengnsys-flask-restx
|
||||
|
||||
The `opengnsys-flask-executor` and `opengnsys-flask-restx` packages are available on the OpenGnsys package server.
|
||||
|
||||
Run with:
|
||||
|
||||
./gitapi.py
|
||||
|
||||
**Note:** Run as `opengnsys`, as it manages the images located in `/opt/opengnsys/images`.
|
||||
|
||||
|
||||
# Documentation
|
||||
|
||||
Python documentation can be generated using a utility like pdoc3 (there are multiple possible alternatives):
|
||||
|
||||
# Install pdoc3
|
||||
pip install --user pdoc3
|
||||
|
||||
# Generate documentation
|
||||
pdoc3 --force --html opengnsys_git_installer.py
|
||||
|
||||
# Operation
|
||||
|
||||
## Requirements
|
||||
|
||||
The gitapi is designed to run within an existing opengnsys environment. It should be installed in an ogrepository.
|
||||
|
||||
|
||||
## API Examples
|
||||
|
||||
### Get list of branches
|
||||
|
||||
$ curl -L http://localhost:5000/repositories/linux/branches
|
||||
{
|
||||
"branches": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
|
||||
### Synchronize with remote repository
|
||||
|
||||
curl --header "Content-Type: application/json" --data '{"remote_repository":"foobar"}' -X POST -L http://localhost:5000/repositories/linux/sync
|
|
@ -0,0 +1,69 @@
|
|||
# Git API
|
||||
|
||||
La `gitapi.py` es una API para OgGit, escrita en Python/Flask.
|
||||
|
||||
Es un servidor HTTP que recibe comandos y ejecuta acciones de mantenimiento incluyendo la creación y eliminación de repositorios.
|
||||
|
||||
|
||||
# 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.
|
||||
|
||||
|
||||
# Uso
|
||||
|
||||
|
||||
## Distribuciones antiguas (18.04)
|
||||
|
||||
sudo apt install -y python3.8 python3.8-venv python3-venv libarchive-dev
|
||||
python3.8 -m venv venvog
|
||||
. venvog/bin/activate
|
||||
python3.8 -m pip install --upgrade pip
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
Ejecutar con:
|
||||
|
||||
./gitapi.py
|
||||
|
||||
|
||||
## Uso
|
||||
|
||||
**Nota:** Ejecutar como `opengnsys`, ya que gestiona las imágenes que se encuentran en `/opt/opengnsys/images`.
|
||||
|
||||
$ . venvog/bin/activate
|
||||
$ ./gitapi.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
|
||||
|
||||
# Funcionamiento
|
||||
|
||||
## Requisitos
|
||||
|
||||
La gitapi esta diseñada para funcionar dentro de un entorno opengnsys existente. Se debe instalar en un ogrepository.
|
||||
|
||||
## Ejemplo de API
|
||||
|
||||
### Obtener lista de ramas
|
||||
|
||||
$ curl -L http://localhost:5000/repositories/linux/branches
|
||||
{
|
||||
"branches": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
|
||||
### Sincronizar con repositorio remoto
|
||||
|
||||
curl --header "Content-Type: application/json" --data '{"remote_repository":"foobar"}' -X POST -L http://localhost:5000/repositories/linux/sync
|
||||
|
|
@ -0,0 +1,633 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
This module provides a Flask-based API for managing Git repositories in the OpenGnsys system.
|
||||
It includes endpoints for creating, deleting, synchronizing, backing up, and performing garbage
|
||||
collection on Git repositories. The API also provides endpoints for retrieving repository
|
||||
information such as the list of repositories and branches, as well as checking the status of
|
||||
asynchronous tasks.
|
||||
|
||||
Classes:
|
||||
None
|
||||
|
||||
Functions:
|
||||
do_repo_backup(repo, params)
|
||||
|
||||
do_repo_sync(repo, params)
|
||||
|
||||
do_repo_gc(repo)
|
||||
|
||||
home()
|
||||
|
||||
get_repositories()
|
||||
|
||||
create_repo(repo)
|
||||
|
||||
sync_repo(repo)
|
||||
|
||||
backup_repository(repo)
|
||||
|
||||
gc_repo(repo)
|
||||
|
||||
tasks_status(task_id)
|
||||
|
||||
delete_repo(repo)
|
||||
|
||||
get_repository_branches(repo)
|
||||
|
||||
health_check()
|
||||
|
||||
Constants:
|
||||
REPOSITORIES_BASE_PATH (str): The base path where Git repositories are stored.
|
||||
|
||||
Global Variables:
|
||||
app (Flask): The Flask application instance.
|
||||
executor (Executor): The Flask-Executor instance for managing asynchronous tasks.
|
||||
tasks (dict): A dictionary to store the status of asynchronous tasks.
|
||||
"""
|
||||
|
||||
# pylint: disable=locally-disabled, line-too-long
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import git
|
||||
import time
|
||||
from opengnsys_git_installer import OpengnsysGitInstaller
|
||||
from flask import Flask, request, jsonify # stream_with_context, Response,
|
||||
from flask_executor import Executor
|
||||
from flask_restx import Api, Resource, fields
|
||||
#from flasgger import Swagger
|
||||
import paramiko
|
||||
import logging
|
||||
import traceback
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from systemd.journal import JournalHandler
|
||||
|
||||
debug_enabled = False
|
||||
|
||||
log = logging.getLogger('gitapi')
|
||||
log.addHandler(JournalHandler())
|
||||
log.setLevel(logging.INFO)
|
||||
log.info("Started")
|
||||
|
||||
|
||||
REPOSITORIES_BASE_PATH = "/opt/opengnsys/ogrepository/oggit/git/oggit/"
|
||||
|
||||
start_time = time.time()
|
||||
tasks = {}
|
||||
tasks_max = 1024
|
||||
|
||||
# Create an instance of the Flask class
|
||||
app = Flask(__name__)
|
||||
api = Api(app,
|
||||
version='0.50',
|
||||
title = "OpenGnsys Git API",
|
||||
description = "API for managing disk images stored in Git",
|
||||
doc = "/swagger/")
|
||||
|
||||
git_ns = api.namespace(name = "oggit", description = "Git operations", path = "/oggit/v1")
|
||||
|
||||
executor = Executor(app)
|
||||
|
||||
|
||||
|
||||
def add_task(future):
|
||||
task_id = uuid.uuid4().hex
|
||||
task_data = {
|
||||
"future" : future,
|
||||
"start_time" : time.time()
|
||||
}
|
||||
|
||||
while len(tasks) >= tasks_max:
|
||||
oldest_task_id = min(tasks, key=lambda k: tasks[k]['start_time'])
|
||||
task = tasks[task_id]["future"]
|
||||
if task.running():
|
||||
log.error("Cancelling still running task %s, maximum task limit of %i reached", task_id, tasks_max)
|
||||
task.cancel()
|
||||
|
||||
del tasks[oldest_task_id]
|
||||
|
||||
tasks[task_id] = task_data
|
||||
return task_id
|
||||
|
||||
def do_repo_backup(repo, params):
|
||||
"""
|
||||
Creates a backup of the specified Git repository and uploads it to a remote server via SFTP.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to back up.
|
||||
params (dict): A dictionary containing the following keys:
|
||||
- ssh_server (str): The SSH server address.
|
||||
- ssh_port (int): The SSH server port.
|
||||
- ssh_user (str): The SSH username.
|
||||
- filename (str): The remote filename where the backup will be stored.
|
||||
|
||||
Returns:
|
||||
bool: True if the backup was successful.
|
||||
"""
|
||||
|
||||
git_repo_path = f"{REPOSITORIES_BASE_PATH}/{repo}.git"
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
ssh.connect(params["ssh_server"], params["ssh_port"], params["ssh_user"])
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
|
||||
with sftp.file(params["filename"], mode='wb+') as remote_file:
|
||||
git_repo.archive(remote_file, format="tar.gz")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def do_repo_sync(repo, params):
|
||||
"""
|
||||
Synchronizes a local Git repository with a remote repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the local repository to synchronize.
|
||||
params (dict): A dictionary containing the remote repository URL with the key "remote_repository".
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries, each containing:
|
||||
- "local_ref" (str): The name of the local reference.
|
||||
- "remote_ref" (str): The name of the remote reference.
|
||||
- "summary" (str): A summary of the push operation for the reference.
|
||||
"""
|
||||
git_repo_path = f"{REPOSITORIES_BASE_PATH}/{repo}.git"
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
|
||||
# Recreate the remote every time, it might change
|
||||
if "backup" in git_repo.remotes:
|
||||
git_repo.delete_remote("backup")
|
||||
|
||||
backup_repo = git_repo.create_remote("backup", params["remote_repository"])
|
||||
pushed_references = backup_repo.push("*:*")
|
||||
results = []
|
||||
|
||||
# This gets returned to the API
|
||||
for ref in pushed_references:
|
||||
results = results + [ {"local_ref" : ref.local_ref.name, "remote_ref" : ref.remote_ref.name, "summary" : ref.summary }]
|
||||
|
||||
return results
|
||||
|
||||
def do_repo_gc(repo):
|
||||
"""
|
||||
Perform garbage collection on the specified Git repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to perform garbage collection on.
|
||||
|
||||
Returns:
|
||||
bool: True if the garbage collection command was executed successfully.
|
||||
"""
|
||||
git_repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
git_repo.git.gc()
|
||||
|
||||
@app.errorhandler(HTTPException)
|
||||
def handle_exception(e):
|
||||
"""Return JSON for HTTP errors.
|
||||
|
||||
We create and log an error UUID for each error, and use journald's additional fields for easier searching.
|
||||
"""
|
||||
# start with the correct headers and status code from the error
|
||||
response = e.get_response()
|
||||
|
||||
errid = uuid.uuid4().hex
|
||||
|
||||
|
||||
if debug_enabled:
|
||||
response = {
|
||||
"errcode": e.code,
|
||||
"errname": e.name,
|
||||
"description": e.description,
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
"errcode" : 500,
|
||||
"errname" : "Internal error",
|
||||
"description": f"Please see the log for error {errid}",
|
||||
"error_id" : errid
|
||||
}
|
||||
|
||||
log.error("Error ID %s: code %i, name %s, description %s", errid, e.code, e.name, e.description, extra = { "error_id" : errid, "errcode" : e.code, "errname" : e.name, "description" : e.description })
|
||||
|
||||
# response.content_type = "application/json"
|
||||
return response
|
||||
|
||||
|
||||
# Define a route for the root URL
|
||||
@api.route('/')
|
||||
class GitLib(Resource):
|
||||
|
||||
@api.doc('home')
|
||||
def get(self):
|
||||
"""
|
||||
Home route that returns a JSON response with a welcome message for the OpenGnsys Git API.
|
||||
|
||||
Returns:
|
||||
Response: A Flask JSON response containing a welcome message.
|
||||
"""
|
||||
log.info("Root URL accessed")
|
||||
|
||||
return {
|
||||
"message": "OpenGnsys Git API"
|
||||
}
|
||||
|
||||
@git_ns.route('/repositories')
|
||||
class GitRepositories(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Retrieve a list of Git repositories.
|
||||
|
||||
This endpoint scans the OpenGnsys image path for directories that
|
||||
appear to be Git repositories (i.e., they contain a "HEAD" file).
|
||||
It returns a JSON response containing the names of these repositories.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a list of repository names or an
|
||||
error message if the repository storage is not found.
|
||||
- 200 OK: When the repositories are successfully retrieved.
|
||||
- 500 Internal Server Error: When the repository storage is not found.
|
||||
|
||||
Example JSON response:
|
||||
{
|
||||
"repositories": ["repo1", "repo2"]
|
||||
}
|
||||
"""
|
||||
|
||||
if not os.path.isdir(REPOSITORIES_BASE_PATH):
|
||||
log.error("Can't list repositories. Repository storage at %s not found", REPOSITORIES_BASE_PATH, extra = {"path" : REPOSITORIES_BASE_PATH})
|
||||
return {"error": "Repository storage not found, git functionality may not be installed."}, 500
|
||||
|
||||
repos = []
|
||||
for entry in os.scandir(REPOSITORIES_BASE_PATH):
|
||||
if entry.is_dir(follow_symlinks=False) and os.path.isfile(os.path.join(entry.path, "HEAD")):
|
||||
name = entry.name
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
|
||||
repos = repos + [name]
|
||||
|
||||
log.info("Returning %i repositories", len(repos))
|
||||
return {
|
||||
"repositories": repos
|
||||
}
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
Create a new Git repository.
|
||||
|
||||
This endpoint creates a new Git repository with the specified name.
|
||||
If the repository already exists, it returns a status message indicating so.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to be created.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status message and HTTP status code.
|
||||
- 200: If the repository already exists.
|
||||
- 201: If the repository is successfully created.
|
||||
"""
|
||||
data = request.json
|
||||
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
repo = data["name"]
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if os.path.isdir(repo_path):
|
||||
log.error("Can't create repository %s, already exists at %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path})
|
||||
return {"status": "Repository already exists"}, 200
|
||||
|
||||
|
||||
installer = OpengnsysGitInstaller()
|
||||
installer.add_forgejo_repo(repo)
|
||||
|
||||
#installer.init_git_repo(repo + ".git")
|
||||
|
||||
log.info("Repository %s created", repo, extra = {"repository" : repo})
|
||||
return {"status": "Repository created"}, 201
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/sync')
|
||||
class GitRepoSync(Resource):
|
||||
def post(self, repo):
|
||||
"""
|
||||
Synchronize a repository with a remote repository.
|
||||
|
||||
This endpoint triggers the synchronization process for a specified repository.
|
||||
It expects a JSON payload with the remote repository details.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to be synchronized.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response indicating the status of the synchronization process.
|
||||
- 200: If the synchronization process has started successfully.
|
||||
- 400: If the request payload is missing or invalid.
|
||||
- 404: If the specified repository is not found.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't sync repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
data = request.json
|
||||
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
if not "remote_repository" in data:
|
||||
log.error("Can't create repository, parameter 'remote_repository' missing")
|
||||
return {"error" : "Parameter 'remote_repository' missing"}, 400
|
||||
|
||||
|
||||
future = executor.submit(do_repo_sync, repo, data)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting synchronization of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/backup')
|
||||
class GitRepoBackup(Resource):
|
||||
def backup_repository(self, repo):
|
||||
"""
|
||||
Backup a specified repository.
|
||||
|
||||
Endpoint: POST /repositories/<repo>/backup
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to back up.
|
||||
|
||||
Request Body (JSON):
|
||||
ssh_port (int, optional): The SSH port to use for the backup. Defaults to 22.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response indicating the status of the backup operation.
|
||||
- If the repository is not found, returns a 404 error with a message.
|
||||
- If the request body is missing, returns a 400 error with a message.
|
||||
- If the backup process starts successfully, returns a 200 status with the task ID.
|
||||
|
||||
Notes:
|
||||
- The repository path is constructed by appending ".git" to the repository name.
|
||||
- The backup operation is performed asynchronously using a thread pool executor.
|
||||
- The task ID of the backup operation is generated using UUID and stored in a global tasks dictionary.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't backup repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
data = request.json
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
|
||||
if not "ssh_port" in data:
|
||||
data["ssh_port"] = 22
|
||||
|
||||
|
||||
future = executor.submit(do_repo_backup, repo, data)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting backup of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
@git_ns.route('/repositories/<repo>/compact', methods=['POST'])
|
||||
class GitRepoCompact(Resource):
|
||||
def post(self, repo):
|
||||
"""
|
||||
Initiates a garbage collection (GC) process for a specified Git repository.
|
||||
|
||||
This endpoint triggers an asynchronous GC task for the given repository.
|
||||
The task is submitted to an executor, and a unique task ID is generated
|
||||
and returned to the client.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to perform GC on.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing the status of the request and
|
||||
a unique task ID if the repository is found, or an error
|
||||
message if the repository is not found.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't compact repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
future = executor.submit(do_repo_gc, repo)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting compaction of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
|
||||
@git_ns.route('/tasks/<task_id>/status')
|
||||
class GitTaskStatus(Resource):
|
||||
def get(self, task_id):
|
||||
"""
|
||||
Endpoint to check the status of a specific task.
|
||||
|
||||
Args:
|
||||
task_id (str): The unique identifier of the task.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing the status of the task.
|
||||
- If the task is not found, returns a 404 error with an error message.
|
||||
- If the task is completed, returns a 200 status with the result.
|
||||
- If the task is still in progress, returns a 202 status indicating the task is in progress.
|
||||
"""
|
||||
if not task_id in tasks:
|
||||
log.error("Task %s was not found", task_id, extra = {"task_id" : task_id})
|
||||
return {"error": "Task not found"}, 404
|
||||
|
||||
future = tasks[task_id]["future"]
|
||||
|
||||
try:
|
||||
if future.done():
|
||||
result = future.result()
|
||||
log.info("Returning completion of task %s", task_id, extra = {"task_id" : task_id})
|
||||
return {"status" : "completed", "result" : result}, 200
|
||||
else:
|
||||
log.info("Task %s is still in progress", task_id, extra = {"task_id" : task_id})
|
||||
return {"status" : "in progress"}, 202
|
||||
except Exception as e:
|
||||
errid = uuid.uuid4().hex
|
||||
|
||||
|
||||
log.error("Task %s failed with exception %s, UUID %s", task_id, traceback.format_exception(e), errid, extra = {"task_id" : task_id, "exception" : traceback.format_exception(e), "error_id" : errid})
|
||||
return {"status" : "internal error", "error_id" : errid }, 500
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>', methods=['DELETE'])
|
||||
class GitRepo(Resource):
|
||||
def delete(self, repo):
|
||||
"""
|
||||
Deletes a Git repository.
|
||||
|
||||
This endpoint deletes a Git repository specified by the `repo` parameter.
|
||||
If the repository does not exist, it returns a 404 error with a message
|
||||
indicating that the repository was not found. If the repository is successfully
|
||||
deleted, it returns a 200 status with a message indicating that the repository
|
||||
was deleted.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to delete.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status message and the appropriate HTTP status code.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't delete repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
shutil.rmtree(repo_path)
|
||||
log.info("Deleted repository %s", repo, extra = {"repository" : repo})
|
||||
return {"status": "Repository deleted"}, 200
|
||||
|
||||
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/branches')
|
||||
class GitRepoBranches(Resource):
|
||||
def get(self, repo):
|
||||
"""
|
||||
Retrieve the list of branches for a given repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of branch names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "branches" key containing a list of branch names.
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" if the repository does not exist.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get branches of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
branches = []
|
||||
for branch in git_repo.branches:
|
||||
branches = branches + [branch.name]
|
||||
|
||||
log.info("Returning %i branches", len(branches))
|
||||
return {
|
||||
"branches": branches
|
||||
}
|
||||
|
||||
@git_ns.route('/repositories/<repo>/branches/<branch>')
|
||||
class GitRepoBranchesDeleter(Resource):
|
||||
def delete(self, repo, branch):
|
||||
"""Delete a given branch in a given repository
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of branch names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "status" key containing "deleted"
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" or "Branch not found"
|
||||
"""
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get branches of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
if not branch in git_repo.branches:
|
||||
log.error("Can't delete branch %s, not found in repository %s", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"error": "Branch not found"}, 404
|
||||
|
||||
git_repo.delete_head(branch)
|
||||
log.info("Branch %s of repository %s deleted", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"status": "deleted"}, 200
|
||||
|
||||
|
||||
@git_ns.route('/health')
|
||||
class GitHealth(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Health check endpoint.
|
||||
|
||||
This endpoint returns a JSON response indicating the health status of the application.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status key set to "OK". Currently it always returns
|
||||
a successful value, but this endpoint can still be used to check that the API is
|
||||
active and functional.
|
||||
|
||||
"""
|
||||
log.info("Health check endpoint called")
|
||||
return {
|
||||
"status": "OK"
|
||||
}
|
||||
|
||||
@git_ns.route('/status')
|
||||
class GitStatus(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Status check endpoint.
|
||||
|
||||
This endpoint returns a JSON response indicating the status of the application.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with status information
|
||||
|
||||
"""
|
||||
log.info("Status endpoint called")
|
||||
|
||||
return {
|
||||
"uptime" : time.time() - start_time,
|
||||
"active_tasks" : len(tasks)
|
||||
}
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
log.info("Request from %s: %s %s %s %s", request.remote_addr, request.method, request.scheme, request.full_path, response.status,
|
||||
extra = {"remote_addr" : request.remote_addr, "method" : request.method, "scheme" : request.scheme, "full_path" : request.full_path, "status" : response.status})
|
||||
|
||||
if debug_enabled:
|
||||
log.debug("Response: %s", response.data, extra = {"response" : response.data})
|
||||
|
||||
return response
|
||||
|
||||
api.add_namespace(git_ns)
|
||||
|
||||
|
||||
|
||||
# Run the Flask app
|
||||
if __name__ == '__main__':
|
||||
print(f"Map: {app.url_map}")
|
||||
app.run(debug=True, host='0.0.0.0')
|
|
@ -0,0 +1 @@
|
|||
../installer/opengnsys_git_installer.py
|
|
@ -0,0 +1,34 @@
|
|||
aniso8601==9.0.1
|
||||
attrs==24.2.0
|
||||
bcrypt==4.2.0
|
||||
blinker==1.8.2
|
||||
cffi==1.17.1
|
||||
click==8.1.7
|
||||
cryptography==43.0.1
|
||||
dataclasses==0.6
|
||||
flasgger==0.9.7.1
|
||||
Flask==3.0.3
|
||||
Flask-Executor==1.0.0
|
||||
flask-restx==1.3.0
|
||||
gitdb==4.0.11
|
||||
GitPython==3.1.43
|
||||
importlib_resources==6.4.5
|
||||
itsdangerous==2.2.0
|
||||
Jinja2==3.1.4
|
||||
jsonschema==4.23.0
|
||||
jsonschema-specifications==2024.10.1
|
||||
libarchive-c==5.1
|
||||
MarkupSafe==3.0.1
|
||||
mistune==3.0.2
|
||||
packaging==24.1
|
||||
paramiko==3.5.0
|
||||
pycparser==2.22
|
||||
PyNaCl==1.5.0
|
||||
pytz==2024.2
|
||||
PyYAML==6.0.2
|
||||
referencing==0.35.1
|
||||
rpds-py==0.20.0
|
||||
six==1.16.0
|
||||
smmap==5.0.1
|
||||
termcolor==2.5.0
|
||||
Werkzeug==3.0.4
|
|
@ -0,0 +1,27 @@
|
|||
bcrypt==4.0.1
|
||||
cffi==1.15.1
|
||||
click==8.0.4
|
||||
colorterm==0.3
|
||||
contextvars==2.4
|
||||
cryptography==40.0.2
|
||||
dataclasses==0.8
|
||||
Flask==2.0.3
|
||||
Flask-Executor==1.0.0
|
||||
gitdb==4.0.9
|
||||
GitPython==3.1.20
|
||||
immutables==0.19
|
||||
importlib-metadata==4.8.3
|
||||
itsdangerous==2.0.1
|
||||
Jinja2==3.0.3
|
||||
libarchive==0.4.7
|
||||
MarkupSafe==2.0.1
|
||||
nose==1.3.7
|
||||
paramiko==3.5.0
|
||||
pkg_resources==0.0.0
|
||||
pycparser==2.21
|
||||
PyNaCl==1.5.0
|
||||
smmap==5.0.0
|
||||
termcolor==1.1.0
|
||||
typing_extensions==4.1.1
|
||||
Werkzeug==2.0.3
|
||||
zipp==3.6.0
|
|
@ -0,0 +1,42 @@
|
|||
# API Server
|
||||
|
||||
`api_server.py` is a Flask script that loads and executes Flask blueprints from the `blueprints` directory.
|
||||
|
||||
|
||||
Currently it's intended to combine oggit and ogrepository.
|
||||
|
||||
|
||||
# Usage
|
||||
|
||||
## Ubuntu 24.04
|
||||
|
||||
sudo apt install -y python3-flask python3-paramiko opengnsys-flask-executor opengnsys-flask-restx
|
||||
|
||||
The `opengnsys-flask-executor` and `opengnsys-flask-restx` packages are available on the OpenGnsys package server.
|
||||
|
||||
Run with:
|
||||
|
||||
./api_server.py
|
||||
|
||||
|
||||
# Operation
|
||||
|
||||
## Requirements
|
||||
|
||||
The gitapi is designed to run within an existing opengnsys environment. It should be installed in an ogrepository.
|
||||
|
||||
|
||||
## API Examples
|
||||
|
||||
### Get list of branches
|
||||
|
||||
$ curl -L http://localhost:5000/repositories/linux/branches
|
||||
{
|
||||
"branches": [
|
||||
"master"
|
||||
]
|
||||
}
|
||||
|
||||
### Synchronize with remote repository
|
||||
|
||||
curl --header "Content-Type: application/json" --data '{"remote_repository":"foobar"}' -X POST -L http://localhost:5000/repositories/linux/sync
|
|
@ -0,0 +1,170 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/usr/share/opengnsys-modules/python3/dist-packages")
|
||||
sys.path.insert(0, "/opt/opengnsys/oggit/bin/")
|
||||
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import uuid
|
||||
import argparse
|
||||
import yaml
|
||||
from flask import Flask, request
|
||||
from flask_executor import Executor
|
||||
from flask_restx import Api
|
||||
from flasgger import Swagger
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from systemd.journal import JournalHandler
|
||||
|
||||
class FakeArgs:
|
||||
def __init__(self):
|
||||
self.verbose = False
|
||||
self.listen = None
|
||||
self.port = None
|
||||
self.debug = None
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="api_server.py",
|
||||
description="OpenGnsys Repository API Server",
|
||||
)
|
||||
|
||||
debug_enabled = False
|
||||
listen_host = '0.0.0.0'
|
||||
listen_port = 8006
|
||||
|
||||
is_gunicorn = "gunicorn" in os.environ.get("SERVER_SOFTWARE", "")
|
||||
|
||||
if not is_gunicorn:
|
||||
# Gunicorn passes us all the arguments passed to gunicorn itself, which of course crashes here since we don't recognize them.
|
||||
# Deal with this by not doing argument handling when running under gunicorn
|
||||
parser.add_argument('--debug', action='store_true', help="Enable debug output")
|
||||
parser.add_argument('--listen', metavar="HOST", help="Listen address")
|
||||
parser.add_argument('--port', metavar="PORT", help="Listen port")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help = "Verbose console output")
|
||||
|
||||
args = parser.parse_args()
|
||||
else:
|
||||
args = FakeArgs()
|
||||
|
||||
|
||||
log = logging.getLogger('api_server')
|
||||
log.addHandler(JournalHandler())
|
||||
|
||||
if args.verbose:
|
||||
log.addHandler(logging.StreamHandler(stream=sys.stderr))
|
||||
log.setLevel(logging.DEBUG)
|
||||
else:
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
|
||||
if is_gunicorn:
|
||||
log.info("Running under gunicorn, argument handling disabled.")
|
||||
|
||||
if args.listen:
|
||||
listen_host = args.listen
|
||||
|
||||
if args.port:
|
||||
listen_port = args.port
|
||||
|
||||
|
||||
if args.debug:
|
||||
debug_enabled = True
|
||||
|
||||
|
||||
api_base_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
blueprints_dir = os.path.join(api_base_dir, 'blueprints')
|
||||
installer_dir = os.path.join(api_base_dir, '../installer')
|
||||
|
||||
|
||||
|
||||
|
||||
sys.path.insert(0, installer_dir)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Create an instance of the Flask class
|
||||
app = Flask(__name__)
|
||||
api = Api(app,
|
||||
version='0.50',
|
||||
title = "OpenGnsys Git API",
|
||||
description = "API for managing disk images stored in Git",
|
||||
doc = "/apidocs/")
|
||||
|
||||
|
||||
executor = Executor(app)
|
||||
|
||||
log.info("Loading blueprints from %s", blueprints_dir)
|
||||
sys.path.insert(0, blueprints_dir)
|
||||
|
||||
for filename in os.listdir(blueprints_dir):
|
||||
if filename.endswith('.py'):
|
||||
|
||||
log.info("Loading %s/%s", blueprints_dir, filename)
|
||||
|
||||
module_name = filename.replace(".py", "")
|
||||
swagger_file = os.path.join(blueprints_dir, filename.replace(".py", ".yaml"))
|
||||
|
||||
log.info("Importing %s", module_name)
|
||||
importlib.invalidate_caches()
|
||||
module = importlib.import_module(module_name)
|
||||
log.debug("Returned: %s", module)
|
||||
|
||||
app.register_blueprint(module.blueprint)
|
||||
|
||||
if os.path.exists(swagger_file):
|
||||
log.info("Loading Swagger documentation from %s...", swagger_file)
|
||||
|
||||
with open(swagger_file, "r", encoding='utf-8') as file:
|
||||
swagger_template = yaml.safe_load(file)
|
||||
|
||||
#print(f"Template: {swagger_template}")
|
||||
#swagger = Swagger(app, template=swagger_template)
|
||||
else:
|
||||
log.warning("Swagger not found for this module, looked in %s", swagger_file)
|
||||
|
||||
|
||||
|
||||
@app.errorhandler(HTTPException)
|
||||
def handle_exception(e):
|
||||
"""Return JSON for HTTP errors.
|
||||
|
||||
We create and log an error UUID for each error, and use journald's additional fields for easier searching.
|
||||
"""
|
||||
# start with the correct headers and status code from the error
|
||||
response = e.get_response()
|
||||
|
||||
errid = uuid.uuid4().hex
|
||||
|
||||
|
||||
response = {
|
||||
"errcode": e.code,
|
||||
"errname": e.name,
|
||||
"description": e.description,
|
||||
}
|
||||
|
||||
log.error("Error ID %s: code %i, name %s, description %s", errid, e.code, e.name, e.description, extra = { "error_id" : errid, "errcode" : e.code, "errname" : e.name, "description" : e.description })
|
||||
|
||||
return response, 500
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
log.info("Request from %s: %s %s %s %s", request.remote_addr, request.method, request.scheme, request.full_path, response.status,
|
||||
extra = {"remote_addr" : request.remote_addr, "method" : request.method, "scheme" : request.scheme, "full_path" : request.full_path, "status" : response.status})
|
||||
|
||||
if debug_enabled:
|
||||
log.debug("Response: %s", response.data, extra = {"response" : response.data})
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
|
||||
# Run the Flask app
|
||||
if __name__ == '__main__':
|
||||
print(f"Map: {app.url_map}")
|
||||
app.run(debug=debug_enabled, host=listen_host, port=listen_port)
|
|
@ -0,0 +1,713 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
This module provides a Flask-based API for managing Git repositories in the OpenGnsys system.
|
||||
It includes endpoints for creating, deleting, synchronizing, backing up, and performing garbage
|
||||
collection on Git repositories. The API also provides endpoints for retrieving repository
|
||||
information such as the list of repositories and branches, as well as checking the status of
|
||||
asynchronous tasks.
|
||||
|
||||
Classes:
|
||||
None
|
||||
|
||||
Functions:
|
||||
do_repo_backup(repo, params)
|
||||
|
||||
do_repo_sync(repo, params)
|
||||
|
||||
do_repo_gc(repo)
|
||||
|
||||
home()
|
||||
|
||||
get_repositories()
|
||||
|
||||
create_repo(repo)
|
||||
|
||||
sync_repo(repo)
|
||||
|
||||
backup_repository(repo)
|
||||
|
||||
gc_repo(repo)
|
||||
|
||||
tasks_status(task_id)
|
||||
|
||||
delete_repo(repo)
|
||||
|
||||
get_repository_branches(repo)
|
||||
|
||||
health_check()
|
||||
|
||||
Constants:
|
||||
REPOSITORIES_BASE_PATH (str): The base path where Git repositories are stored.
|
||||
|
||||
Global Variables:
|
||||
app (Flask): The Flask application instance.
|
||||
executor (Executor): The Flask-Executor instance for managing asynchronous tasks.
|
||||
tasks (dict): A dictionary to store the status of asynchronous tasks.
|
||||
"""
|
||||
|
||||
# pylint: disable=locally-disabled, line-too-long
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import git
|
||||
from opengnsys_git_installer import OpengnsysGitInstaller
|
||||
from flask import Blueprint, request
|
||||
from flask_restx import Resource, Api
|
||||
import paramiko
|
||||
from systemd.journal import JournalHandler
|
||||
|
||||
|
||||
debug_enabled = False
|
||||
|
||||
log = logging.getLogger('gitapi')
|
||||
log.addHandler(JournalHandler())
|
||||
log.setLevel(logging.INFO)
|
||||
log.info("Started")
|
||||
|
||||
|
||||
REPOSITORIES_BASE_PATH = "/opt/opengnsys/ogrepository/oggit/git/oggit/"
|
||||
|
||||
start_time = time.time()
|
||||
tasks = {}
|
||||
tasks_max = 1024
|
||||
|
||||
blueprint = Blueprint('git_api', __name__, template_folder='templates', url_prefix = '/oggit/v1')
|
||||
api = Api(blueprint)
|
||||
git_ns = api
|
||||
|
||||
|
||||
def add_task(future):
|
||||
task_id = uuid.uuid4().hex
|
||||
task_data = {
|
||||
"future" : future,
|
||||
"start_time" : time.time()
|
||||
}
|
||||
|
||||
while len(tasks) >= tasks_max:
|
||||
oldest_task_id = min(tasks, key=lambda k: tasks[k]['start_time'])
|
||||
task = tasks[task_id]["future"]
|
||||
if task.running():
|
||||
log.error("Cancelling still running task %s, maximum task limit of %i reached", task_id, tasks_max)
|
||||
task.cancel()
|
||||
|
||||
del tasks[oldest_task_id]
|
||||
|
||||
tasks[task_id] = task_data
|
||||
return task_id
|
||||
|
||||
def do_repo_backup(repo, params):
|
||||
"""
|
||||
Creates a backup of the specified Git repository and uploads it to a remote server via SFTP.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to back up.
|
||||
params (dict): A dictionary containing the following keys:
|
||||
- ssh_server (str): The SSH server address.
|
||||
- ssh_port (int): The SSH server port.
|
||||
- ssh_user (str): The SSH username.
|
||||
- filename (str): The remote filename where the backup will be stored.
|
||||
|
||||
Returns:
|
||||
bool: True if the backup was successful.
|
||||
"""
|
||||
|
||||
git_repo_path = f"{REPOSITORIES_BASE_PATH}/{repo}.git"
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
ssh.connect(params["ssh_server"], params["ssh_port"], params["ssh_user"])
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
|
||||
with sftp.file(params["filename"], mode='wb+') as remote_file:
|
||||
git_repo.archive(remote_file, format="tar.gz")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def do_repo_sync(repo, params):
|
||||
"""
|
||||
Synchronizes a local Git repository with a remote repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the local repository to synchronize.
|
||||
params (dict): A dictionary containing the remote repository URL with the key "remote_repository".
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries, each containing:
|
||||
- "local_ref" (str): The name of the local reference.
|
||||
- "remote_ref" (str): The name of the remote reference.
|
||||
- "summary" (str): A summary of the push operation for the reference.
|
||||
"""
|
||||
git_repo_path = f"{REPOSITORIES_BASE_PATH}/{repo}.git"
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
|
||||
# Recreate the remote every time, it might change
|
||||
if "backup" in git_repo.remotes:
|
||||
git_repo.delete_remote("backup")
|
||||
|
||||
backup_repo = git_repo.create_remote("backup", params["remote_repository"])
|
||||
pushed_references = backup_repo.push("*:*")
|
||||
results = []
|
||||
|
||||
# This gets returned to the API
|
||||
for ref in pushed_references:
|
||||
results = results + [ {"local_ref" : ref.local_ref.name, "remote_ref" : ref.remote_ref.name, "summary" : ref.summary }]
|
||||
|
||||
return results
|
||||
|
||||
def do_repo_gc(repo):
|
||||
"""
|
||||
Perform garbage collection on the specified Git repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to perform garbage collection on.
|
||||
|
||||
Returns:
|
||||
bool: True if the garbage collection command was executed successfully.
|
||||
"""
|
||||
git_repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
git_repo.git.gc()
|
||||
|
||||
|
||||
|
||||
# Define a route for the root URL
|
||||
@api.route('/')
|
||||
class GitLib(Resource):
|
||||
|
||||
#@api.doc('home')
|
||||
def get(self):
|
||||
"""
|
||||
Home route that returns a JSON response with a welcome message for the OpenGnsys Git API.
|
||||
|
||||
Returns:
|
||||
Response: A Flask JSON response containing a welcome message.
|
||||
"""
|
||||
log.info("Root URL accessed")
|
||||
|
||||
return {
|
||||
"message": "OpenGnsys Git API"
|
||||
}
|
||||
|
||||
@git_ns.route('/repositories')
|
||||
class GitRepositories(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Retrieve a list of Git repositories.
|
||||
|
||||
This endpoint scans the OpenGnsys image path for directories that
|
||||
appear to be Git repositories (i.e., they contain a "HEAD" file).
|
||||
It returns a JSON response containing the names of these repositories.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a list of repository names or an
|
||||
error message if the repository storage is not found.
|
||||
- 200 OK: When the repositories are successfully retrieved.
|
||||
- 500 Internal Server Error: When the repository storage is not found.
|
||||
|
||||
Example JSON response:
|
||||
{
|
||||
"repositories": ["repo1", "repo2"]
|
||||
}
|
||||
"""
|
||||
|
||||
if not os.path.isdir(REPOSITORIES_BASE_PATH):
|
||||
log.error("Can't list repositories. Repository storage at %s not found", REPOSITORIES_BASE_PATH, extra = {"path" : REPOSITORIES_BASE_PATH})
|
||||
return {"error": "Repository storage not found, git functionality may not be installed."}, 500
|
||||
|
||||
repos = []
|
||||
for entry in os.scandir(REPOSITORIES_BASE_PATH):
|
||||
if entry.is_dir(follow_symlinks=False) and os.path.isfile(os.path.join(entry.path, "HEAD")):
|
||||
name = entry.name
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
|
||||
repos = repos + [name]
|
||||
|
||||
log.info("Returning %i repositories", len(repos))
|
||||
return {
|
||||
"repositories": repos
|
||||
}
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
Create a new Git repository.
|
||||
|
||||
This endpoint creates a new Git repository with the specified name.
|
||||
If the repository already exists, it returns a status message indicating so.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to be created.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status message and HTTP status code.
|
||||
- 200: If the repository already exists.
|
||||
- 201: If the repository is successfully created.
|
||||
"""
|
||||
data = request.json
|
||||
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
repo = data["name"]
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if os.path.isdir(repo_path):
|
||||
log.error("Can't create repository %s, already exists at %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path})
|
||||
return {"status": "Repository already exists"}, 200
|
||||
|
||||
|
||||
installer = OpengnsysGitInstaller()
|
||||
installer.add_forgejo_repo(repo)
|
||||
|
||||
#installer.init_git_repo(repo + ".git")
|
||||
|
||||
log.info("Repository %s created", repo, extra = {"repository" : repo})
|
||||
return {"status": "Repository created"}, 201
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/sync')
|
||||
class GitRepoSync(Resource):
|
||||
def post(self, repo):
|
||||
"""
|
||||
Synchronize a repository with a remote repository.
|
||||
|
||||
This endpoint triggers the synchronization process for a specified repository.
|
||||
It expects a JSON payload with the remote repository details.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to be synchronized.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response indicating the status of the synchronization process.
|
||||
- 200: If the synchronization process has started successfully.
|
||||
- 400: If the request payload is missing or invalid.
|
||||
- 404: If the specified repository is not found.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't sync repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
data = request.json
|
||||
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
if not "remote_repository" in data:
|
||||
log.error("Can't create repository, parameter 'remote_repository' missing")
|
||||
return {"error" : "Parameter 'remote_repository' missing"}, 400
|
||||
|
||||
|
||||
future = executor.submit(do_repo_sync, repo, data)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting synchronization of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/backup')
|
||||
class GitRepoBackup(Resource):
|
||||
def backup_repository(self, repo):
|
||||
"""
|
||||
Backup a specified repository.
|
||||
|
||||
Endpoint: POST /repositories/<repo>/backup
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to back up.
|
||||
|
||||
Request Body (JSON):
|
||||
ssh_port (int, optional): The SSH port to use for the backup. Defaults to 22.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response indicating the status of the backup operation.
|
||||
- If the repository is not found, returns a 404 error with a message.
|
||||
- If the request body is missing, returns a 400 error with a message.
|
||||
- If the backup process starts successfully, returns a 200 status with the task ID.
|
||||
|
||||
Notes:
|
||||
- The repository path is constructed by appending ".git" to the repository name.
|
||||
- The backup operation is performed asynchronously using a thread pool executor.
|
||||
- The task ID of the backup operation is generated using UUID and stored in a global tasks dictionary.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't backup repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
data = request.json
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
|
||||
if not "ssh_port" in data:
|
||||
data["ssh_port"] = 22
|
||||
|
||||
|
||||
future = executor.submit(do_repo_backup, repo, data)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting backup of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
@git_ns.route('/repositories/<repo>/compact', methods=['POST'])
|
||||
class GitRepoCompact(Resource):
|
||||
def post(self, repo):
|
||||
"""
|
||||
Initiates a garbage collection (GC) process for a specified Git repository.
|
||||
|
||||
This endpoint triggers an asynchronous GC task for the given repository.
|
||||
The task is submitted to an executor, and a unique task ID is generated
|
||||
and returned to the client.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to perform GC on.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing the status of the request and
|
||||
a unique task ID if the repository is found, or an error
|
||||
message if the repository is not found.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't compact repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
future = executor.submit(do_repo_gc, repo)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting compaction of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
|
||||
@git_ns.route('/tasks/<task_id>/status')
|
||||
class GitTaskStatus(Resource):
|
||||
def get(self, task_id):
|
||||
"""
|
||||
Endpoint to check the status of a specific task.
|
||||
|
||||
Args:
|
||||
task_id (str): The unique identifier of the task.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing the status of the task.
|
||||
- If the task is not found, returns a 404 error with an error message.
|
||||
- If the task is completed, returns a 200 status with the result.
|
||||
- If the task is still in progress, returns a 202 status indicating the task is in progress.
|
||||
"""
|
||||
if not task_id in tasks:
|
||||
log.error("Task %s was not found", task_id, extra = {"task_id" : task_id})
|
||||
return {"error": "Task not found"}, 404
|
||||
|
||||
future = tasks[task_id]["future"]
|
||||
|
||||
try:
|
||||
if future.done():
|
||||
result = future.result()
|
||||
log.info("Returning completion of task %s", task_id, extra = {"task_id" : task_id})
|
||||
return {"status" : "completed", "result" : result}, 200
|
||||
else:
|
||||
log.info("Task %s is still in progress", task_id, extra = {"task_id" : task_id})
|
||||
return {"status" : "in progress"}, 202
|
||||
except Exception as e:
|
||||
errid = uuid.uuid4().hex
|
||||
|
||||
|
||||
log.error("Task %s failed with exception %s, UUID %s", task_id, traceback.format_exception(e), errid, extra = {"task_id" : task_id, "exception" : traceback.format_exception(e), "error_id" : errid})
|
||||
return {"status" : "internal error", "error_id" : errid }, 500
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>', methods=['DELETE'])
|
||||
class GitRepo(Resource):
|
||||
def delete(self, repo):
|
||||
"""
|
||||
Deletes a Git repository.
|
||||
|
||||
This endpoint deletes a Git repository specified by the `repo` parameter.
|
||||
If the repository does not exist, it returns a 404 error with a message
|
||||
indicating that the repository was not found. If the repository is successfully
|
||||
deleted, it returns a 200 status with a message indicating that the repository
|
||||
was deleted.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to delete.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status message and the appropriate HTTP status code.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't delete repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
shutil.rmtree(repo_path)
|
||||
log.info("Deleted repository %s", repo, extra = {"repository" : repo})
|
||||
return {"status": "Repository deleted"}, 200
|
||||
|
||||
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/branches')
|
||||
class GitRepoBranches(Resource):
|
||||
def get(self, repo):
|
||||
"""
|
||||
Retrieve the list of branches for a given repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of branch names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "branches" key containing a list of branch names.
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" if the repository does not exist.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get branches of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
branches = []
|
||||
for branch in git_repo.branches:
|
||||
branches = branches + [branch.name]
|
||||
|
||||
log.info("Returning %i branches", len(branches))
|
||||
return {
|
||||
"branches": branches
|
||||
}
|
||||
|
||||
@git_ns.route('/repositories/<repo>/branches/<branch>')
|
||||
class GitRepoBranchesDeleter(Resource):
|
||||
def delete(self, repo, branch):
|
||||
"""Delete a given branch in a given repository
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of branch names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "status" key containing "deleted"
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" or "Branch not found"
|
||||
"""
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get branches of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
if not branch in git_repo.branches:
|
||||
log.error("Can't delete branch %s, not found in repository %s", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"error": "Branch not found"}, 404
|
||||
|
||||
git_repo.delete_head(branch)
|
||||
log.info("Branch %s of repository %s deleted", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"status": "deleted"}, 200
|
||||
|
||||
def post(self, repo, branch):
|
||||
"""Create a given branch in a given repository
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of branch names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "status" key containing "deleted"
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" or "Branch not found"
|
||||
- 409: A JSON object with an "error" key containing the message "Branch already exists"
|
||||
"""
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get branches of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
data = request.json
|
||||
if data is None:
|
||||
log.error("Can't create branch, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
if not "commit" in data:
|
||||
log.error("Can't create branch, commit parameter missing")
|
||||
return {"error" : "commit parameter missing"}, 400
|
||||
|
||||
|
||||
if branch in git_repo.branches:
|
||||
log.error("Can't create branch %s, already found in repository %s", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"error": "Branch already exists"}, 409
|
||||
|
||||
git_repo.create_head(branch, commit = data["commit"] )
|
||||
log.info("Branch %s of repository %s created", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"status": "created"}, 200
|
||||
|
||||
@git_ns.route('/repositories/<repo>/tags')
|
||||
class GitRepoTags(Resource):
|
||||
def get(self, repo):
|
||||
"""
|
||||
Retrieve the list of tags for a given repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of tags names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "tags" key containing a list of tags names.
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" if the repository does not exist.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get tags of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
tags = []
|
||||
for tag in git_repo.tags:
|
||||
tags = tags + [tag.name]
|
||||
|
||||
log.info("Returning %i tags", len(tags))
|
||||
return {
|
||||
"tags": tags
|
||||
}
|
||||
|
||||
@git_ns.route('/repositories/<repo>/tags/<tag>')
|
||||
class GitRepoTagsDeleter(Resource):
|
||||
def delete(self, repo, tag):
|
||||
"""Delete a given tag in a given repository
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of tag names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "status" key containing "deleted"
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" or "Tag not found"
|
||||
"""
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get tags of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
if not tag in git_repo.tags:
|
||||
log.error("Can't delete tag %s, not found in repository %s", tag, repo, extra = {"repository" : repo, "tag" : tag})
|
||||
return {"error": "Tag not found"}, 404
|
||||
|
||||
git_repo.delete_head(tag)
|
||||
log.info("Tag %s of repository %s deleted", tag, repo, extra = {"repository" : repo, "tag" : tag})
|
||||
return {"status": "deleted"}, 200
|
||||
|
||||
def post(self, repo, tag):
|
||||
"""Create a given tag in a given repository
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a creation status
|
||||
- 200: A JSON object with a "status" key containing "created"
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found"
|
||||
- 409: A JSON object with an "error" key containing the message "Tag already exists"
|
||||
"""
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get tags of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
data = request.json
|
||||
if data is None:
|
||||
log.error("Can't create tag, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
if not "commit" in data:
|
||||
log.error("Can't create tag, commit parameter missing")
|
||||
return {"error" : "commit parameter missing"}, 400
|
||||
|
||||
|
||||
if tag in git_repo.tags:
|
||||
log.error("Can't create tag %s, already found in repository %s", tag, repo, extra = {"repository" : repo, "tag" : tag})
|
||||
return {"error": "Tag already exists"}, 409
|
||||
git_repo.create_tag(tag, ref = data["commit"])
|
||||
|
||||
log.info("Tag %s of repository %s created", tag, repo, extra = {"repository" : repo, "tag" : tag})
|
||||
return {"status": "created"}, 200
|
||||
|
||||
@git_ns.route('/health')
|
||||
class GitHealth(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Health check endpoint.
|
||||
|
||||
This endpoint returns a JSON response indicating the health status of the application.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status key set to "OK". Currently it always returns
|
||||
a successful value, but this endpoint can still be used to check that the API is
|
||||
active and functional.
|
||||
|
||||
"""
|
||||
log.info("Health check endpoint called")
|
||||
return {
|
||||
"status": "OK"
|
||||
}
|
||||
|
||||
@git_ns.route('/status')
|
||||
class GitStatus(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Status check endpoint.
|
||||
|
||||
This endpoint returns a JSON response indicating the status of the application.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with status information
|
||||
|
||||
"""
|
||||
log.info("Status endpoint called")
|
||||
|
||||
return {
|
||||
"uptime" : time.time() - start_time,
|
||||
"active_tasks" : len(tasks)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,5 @@
|
|||
oggit (0.5) UNRELEASED; urgency=medium
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- OpenGnsys <opengnsys@opengnsys.es> Fri, 14 Mar 2025 08:40:35 +0100
|
|
@ -1,4 +1,4 @@
|
|||
Source: opengnsys-gitinstaller
|
||||
Source: oggit
|
||||
Section: unknown
|
||||
Priority: optional
|
||||
Maintainer: OpenGnsys <opengnsys@opengnsys.es>
|
||||
|
@ -10,7 +10,7 @@ Homepage: https://opengnsys.es
|
|||
#Vcs-Browser: https://salsa.debian.org/debian/ogboot
|
||||
#Vcs-Git: https://salsa.debian.org/debian/ogboot.git
|
||||
|
||||
Package: opengnsys-gitinstaller
|
||||
Package: oggit
|
||||
Architecture: any
|
||||
Multi-Arch: foreign
|
||||
Depends:
|
||||
|
@ -18,12 +18,21 @@ Depends:
|
|||
${misc:Depends},
|
||||
bsdextrautils,
|
||||
debconf (>= 1.5.0),
|
||||
gunicorn,
|
||||
opengnsys-flask-executor,
|
||||
opengnsys-flask-restx,
|
||||
opengnsys-libarchive-c,
|
||||
python3,
|
||||
python3-aniso8601,
|
||||
python3-flasgger,
|
||||
python3-flask,
|
||||
python3-flask,
|
||||
python3-git,
|
||||
python3-paramiko,
|
||||
python3-requests,
|
||||
python3-termcolor,
|
||||
python3-tqdm
|
||||
python3-tqdm,
|
||||
opengnsys-forgejo (>= 0.5)
|
||||
Conflicts:
|
||||
Description: Opengnsys installer library for OgGit
|
||||
Description: Opengnsys Oggit package
|
||||
Files for OpenGnsys Git support
|
|
@ -0,0 +1,3 @@
|
|||
README.source
|
||||
README.Debian
|
||||
README
|
|
@ -0,0 +1,3 @@
|
|||
/opt/opengnsys/images/git
|
||||
/opt/opengnsys/ogrepository/oggit
|
||||
/opt/opengnsys/ogrepository/oggit/api
|
|
@ -0,0 +1,6 @@
|
|||
api_server.py /opt/opengnsys/ogrepository/oggit/api
|
||||
../installer/opengnsys_git_installer.py /opt/opengnsys/oggit/bin
|
||||
blueprints/gitapi.py /opt/opengnsys/ogrepository/oggit/api/blueprints
|
||||
blueprints/repo_api.py /opt/opengnsys/ogrepository/oggit/api/blueprints
|
||||
opengnsys-repository-api.service /etc/systemd/system
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/bash
|
||||
/usr/bin/systemctl daemon-reload
|
||||
/usr/bin/systemctl enable --now opengnsys-repository-api
|
|
@ -0,0 +1,12 @@
|
|||
Package: oggit
|
||||
Version: 0.5
|
||||
Architecture: amd64
|
||||
Maintainer: OpenGnsys <opengnsys@opengnsys.es>
|
||||
Installed-Size: 193
|
||||
Depends: bsdextrautils, debconf (>= 1.5.0), gunicorn, opengnsys-flask-executor, opengnsys-flask-restx, opengnsys-libarchive-c, python3, python3-aniso8601, python3-flasgger, python3-flask, python3-git, python3-paramiko, python3-requests, python3-termcolor, python3-tqdm
|
||||
Section: unknown
|
||||
Priority: optional
|
||||
Multi-Arch: foreign
|
||||
Homepage: https://opengnsys.es
|
||||
Description: Opengnsys Oggit package
|
||||
Files for OpenGnsys Git support
|
|
@ -0,0 +1,6 @@
|
|||
1a0024adb1d5e54ecff27759c5ac4a7d opt/opengnsys/oggit/bin/api_server.py
|
||||
bd0a968737c2d62ce44490414426ccbb opt/opengnsys/oggit/bin/opengnsys_git_installer.py
|
||||
af5f26474949def90af8794458f3f08d opt/opengnsys/oggit/blueprints/gitapi.py
|
||||
61618848e4caca8b22e3cc7b9c8706b8 opt/opengnsys/oggit/blueprints/repo_api.py
|
||||
48b531f72dec218fcdd61dce26f6b5ab usr/share/doc/oggit/changelog.gz
|
||||
8a13e4a3eb6149d56094319bbed84d0c usr/share/doc/oggit/copyright
|
|
@ -0,0 +1,139 @@
|
|||
#!/usr/bin/env python3
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/usr/share/opengnsys-modules/python3/dist-packages")
|
||||
|
||||
|
||||
import importlib
|
||||
import logging
|
||||
import uuid
|
||||
import argparse
|
||||
from flask import Flask, request
|
||||
from flask_executor import Executor
|
||||
from flask_restx import Api
|
||||
from werkzeug.exceptions import HTTPException
|
||||
from systemd.journal import JournalHandler
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="api_server.py",
|
||||
description="OpenGnsys Repository API Server",
|
||||
)
|
||||
|
||||
debug_enabled = False
|
||||
listen_host = '0.0.0.0'
|
||||
parser.add_argument('--debug', action='store_true', help="Enable debug output")
|
||||
parser.add_argument('--listen', metavar="HOST", help="Listen address")
|
||||
parser.add_argument("-v", "--verbose", action="store_true", help = "Verbose console output")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
|
||||
log = logging.getLogger('api_server')
|
||||
log.addHandler(JournalHandler())
|
||||
|
||||
if args.verbose:
|
||||
log.addHandler(logging.StreamHandler(stream=sys.stderr))
|
||||
log.setLevel(logging.DEBUG)
|
||||
else:
|
||||
log.setLevel(logging.INFO)
|
||||
|
||||
if args.listen:
|
||||
listen_host = args.listen
|
||||
|
||||
|
||||
if args.debug:
|
||||
debug_enabled = True
|
||||
|
||||
|
||||
api_base_dir = os.path.dirname(os.path.realpath(__file__))
|
||||
blueprints_dir = os.path.join(api_base_dir, 'blueprints')
|
||||
installer_dir = os.path.join(api_base_dir, '../installer')
|
||||
|
||||
|
||||
|
||||
|
||||
sys.path.insert(0, installer_dir)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Create an instance of the Flask class
|
||||
app = Flask(__name__)
|
||||
api = Api(app,
|
||||
version='0.50',
|
||||
title = "OpenGnsys Git API",
|
||||
description = "API for managing disk images stored in Git",
|
||||
doc = "/swagger/")
|
||||
|
||||
|
||||
executor = Executor(app)
|
||||
|
||||
log.info("Loading blueprints from %s", blueprints_dir)
|
||||
sys.path.insert(0, blueprints_dir)
|
||||
|
||||
for filename in os.listdir(blueprints_dir):
|
||||
if filename.endswith('.py'):
|
||||
|
||||
log.info("Loading %s/%s", blueprints_dir, filename)
|
||||
|
||||
module_name = filename.replace(".py", "")
|
||||
|
||||
log.info("Importing %s", module_name)
|
||||
importlib.invalidate_caches()
|
||||
module = importlib.import_module(module_name)
|
||||
log.debug("Returned: %s", module)
|
||||
|
||||
app.register_blueprint(module.blueprint)
|
||||
|
||||
|
||||
@app.errorhandler(HTTPException)
|
||||
def handle_exception(e):
|
||||
"""Return JSON for HTTP errors.
|
||||
|
||||
We create and log an error UUID for each error, and use journald's additional fields for easier searching.
|
||||
"""
|
||||
# start with the correct headers and status code from the error
|
||||
response = e.get_response()
|
||||
|
||||
errid = uuid.uuid4().hex
|
||||
|
||||
|
||||
if debug_enabled:
|
||||
response = {
|
||||
"errcode": e.code,
|
||||
"errname": e.name,
|
||||
"description": e.description,
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
"errcode" : 500,
|
||||
"errname" : "Internal error",
|
||||
"description": f"Please see the log for error {errid}",
|
||||
"error_id" : errid
|
||||
}
|
||||
|
||||
log.error("Error ID %s: code %i, name %s, description %s", errid, e.code, e.name, e.description, extra = { "error_id" : errid, "errcode" : e.code, "errname" : e.name, "description" : e.description })
|
||||
|
||||
return response, 500
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
log.info("Request from %s: %s %s %s %s", request.remote_addr, request.method, request.scheme, request.full_path, response.status,
|
||||
extra = {"remote_addr" : request.remote_addr, "method" : request.method, "scheme" : request.scheme, "full_path" : request.full_path, "status" : response.status})
|
||||
|
||||
if debug_enabled:
|
||||
log.debug("Response: %s", response.data, extra = {"response" : response.data})
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
|
||||
# Run the Flask app
|
||||
if __name__ == '__main__':
|
||||
print(f"Map: {app.url_map}")
|
||||
app.run(debug=debug_enabled, host=listen_host)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,573 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
This module provides a Flask-based API for managing Git repositories in the OpenGnsys system.
|
||||
It includes endpoints for creating, deleting, synchronizing, backing up, and performing garbage
|
||||
collection on Git repositories. The API also provides endpoints for retrieving repository
|
||||
information such as the list of repositories and branches, as well as checking the status of
|
||||
asynchronous tasks.
|
||||
|
||||
Classes:
|
||||
None
|
||||
|
||||
Functions:
|
||||
do_repo_backup(repo, params)
|
||||
|
||||
do_repo_sync(repo, params)
|
||||
|
||||
do_repo_gc(repo)
|
||||
|
||||
home()
|
||||
|
||||
get_repositories()
|
||||
|
||||
create_repo(repo)
|
||||
|
||||
sync_repo(repo)
|
||||
|
||||
backup_repository(repo)
|
||||
|
||||
gc_repo(repo)
|
||||
|
||||
tasks_status(task_id)
|
||||
|
||||
delete_repo(repo)
|
||||
|
||||
get_repository_branches(repo)
|
||||
|
||||
health_check()
|
||||
|
||||
Constants:
|
||||
REPOSITORIES_BASE_PATH (str): The base path where Git repositories are stored.
|
||||
|
||||
Global Variables:
|
||||
app (Flask): The Flask application instance.
|
||||
executor (Executor): The Flask-Executor instance for managing asynchronous tasks.
|
||||
tasks (dict): A dictionary to store the status of asynchronous tasks.
|
||||
"""
|
||||
|
||||
# pylint: disable=locally-disabled, line-too-long
|
||||
|
||||
import os.path
|
||||
import os
|
||||
import shutil
|
||||
import uuid
|
||||
import time
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import git
|
||||
from opengnsys_git_installer import OpengnsysGitInstaller
|
||||
from flask import Blueprint, request
|
||||
from flask_restx import Resource, Api
|
||||
import paramiko
|
||||
from systemd.journal import JournalHandler
|
||||
|
||||
|
||||
debug_enabled = False
|
||||
|
||||
log = logging.getLogger('gitapi')
|
||||
log.addHandler(JournalHandler())
|
||||
log.setLevel(logging.INFO)
|
||||
log.info("Started")
|
||||
|
||||
|
||||
REPOSITORIES_BASE_PATH = "/opt/opengnsys/ogrepository/oggit/git/oggit/"
|
||||
|
||||
start_time = time.time()
|
||||
tasks = {}
|
||||
tasks_max = 1024
|
||||
|
||||
blueprint = Blueprint('git_api', __name__, template_folder='templates', url_prefix = '/oggit/v1')
|
||||
api = Api(blueprint)
|
||||
git_ns = api
|
||||
|
||||
|
||||
def add_task(future):
|
||||
task_id = uuid.uuid4().hex
|
||||
task_data = {
|
||||
"future" : future,
|
||||
"start_time" : time.time()
|
||||
}
|
||||
|
||||
while len(tasks) >= tasks_max:
|
||||
oldest_task_id = min(tasks, key=lambda k: tasks[k]['start_time'])
|
||||
task = tasks[task_id]["future"]
|
||||
if task.running():
|
||||
log.error("Cancelling still running task %s, maximum task limit of %i reached", task_id, tasks_max)
|
||||
task.cancel()
|
||||
|
||||
del tasks[oldest_task_id]
|
||||
|
||||
tasks[task_id] = task_data
|
||||
return task_id
|
||||
|
||||
def do_repo_backup(repo, params):
|
||||
"""
|
||||
Creates a backup of the specified Git repository and uploads it to a remote server via SFTP.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to back up.
|
||||
params (dict): A dictionary containing the following keys:
|
||||
- ssh_server (str): The SSH server address.
|
||||
- ssh_port (int): The SSH server port.
|
||||
- ssh_user (str): The SSH username.
|
||||
- filename (str): The remote filename where the backup will be stored.
|
||||
|
||||
Returns:
|
||||
bool: True if the backup was successful.
|
||||
"""
|
||||
|
||||
git_repo_path = f"{REPOSITORIES_BASE_PATH}/{repo}.git"
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
|
||||
ssh.connect(params["ssh_server"], params["ssh_port"], params["ssh_user"])
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
|
||||
with sftp.file(params["filename"], mode='wb+') as remote_file:
|
||||
git_repo.archive(remote_file, format="tar.gz")
|
||||
|
||||
|
||||
return True
|
||||
|
||||
def do_repo_sync(repo, params):
|
||||
"""
|
||||
Synchronizes a local Git repository with a remote repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the local repository to synchronize.
|
||||
params (dict): A dictionary containing the remote repository URL with the key "remote_repository".
|
||||
|
||||
Returns:
|
||||
list: A list of dictionaries, each containing:
|
||||
- "local_ref" (str): The name of the local reference.
|
||||
- "remote_ref" (str): The name of the remote reference.
|
||||
- "summary" (str): A summary of the push operation for the reference.
|
||||
"""
|
||||
git_repo_path = f"{REPOSITORIES_BASE_PATH}/{repo}.git"
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
|
||||
# Recreate the remote every time, it might change
|
||||
if "backup" in git_repo.remotes:
|
||||
git_repo.delete_remote("backup")
|
||||
|
||||
backup_repo = git_repo.create_remote("backup", params["remote_repository"])
|
||||
pushed_references = backup_repo.push("*:*")
|
||||
results = []
|
||||
|
||||
# This gets returned to the API
|
||||
for ref in pushed_references:
|
||||
results = results + [ {"local_ref" : ref.local_ref.name, "remote_ref" : ref.remote_ref.name, "summary" : ref.summary }]
|
||||
|
||||
return results
|
||||
|
||||
def do_repo_gc(repo):
|
||||
"""
|
||||
Perform garbage collection on the specified Git repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to perform garbage collection on.
|
||||
|
||||
Returns:
|
||||
bool: True if the garbage collection command was executed successfully.
|
||||
"""
|
||||
git_repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
git_repo = git.Repo(git_repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', git_repo_path)
|
||||
|
||||
git_repo.git.gc()
|
||||
|
||||
|
||||
|
||||
# Define a route for the root URL
|
||||
@api.route('/')
|
||||
class GitLib(Resource):
|
||||
|
||||
#@api.doc('home')
|
||||
def get(self):
|
||||
"""
|
||||
Home route that returns a JSON response with a welcome message for the OpenGnsys Git API.
|
||||
|
||||
Returns:
|
||||
Response: A Flask JSON response containing a welcome message.
|
||||
"""
|
||||
log.info("Root URL accessed")
|
||||
|
||||
return {
|
||||
"message": "OpenGnsys Git API"
|
||||
}
|
||||
|
||||
@git_ns.route('/repositories')
|
||||
class GitRepositories(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Retrieve a list of Git repositories.
|
||||
|
||||
This endpoint scans the OpenGnsys image path for directories that
|
||||
appear to be Git repositories (i.e., they contain a "HEAD" file).
|
||||
It returns a JSON response containing the names of these repositories.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a list of repository names or an
|
||||
error message if the repository storage is not found.
|
||||
- 200 OK: When the repositories are successfully retrieved.
|
||||
- 500 Internal Server Error: When the repository storage is not found.
|
||||
|
||||
Example JSON response:
|
||||
{
|
||||
"repositories": ["repo1", "repo2"]
|
||||
}
|
||||
"""
|
||||
|
||||
if not os.path.isdir(REPOSITORIES_BASE_PATH):
|
||||
log.error("Can't list repositories. Repository storage at %s not found", REPOSITORIES_BASE_PATH, extra = {"path" : REPOSITORIES_BASE_PATH})
|
||||
return {"error": "Repository storage not found, git functionality may not be installed."}, 500
|
||||
|
||||
repos = []
|
||||
for entry in os.scandir(REPOSITORIES_BASE_PATH):
|
||||
if entry.is_dir(follow_symlinks=False) and os.path.isfile(os.path.join(entry.path, "HEAD")):
|
||||
name = entry.name
|
||||
if name.endswith(".git"):
|
||||
name = name[:-4]
|
||||
|
||||
repos = repos + [name]
|
||||
|
||||
log.info("Returning %i repositories", len(repos))
|
||||
return {
|
||||
"repositories": repos
|
||||
}
|
||||
|
||||
def post(self):
|
||||
"""
|
||||
Create a new Git repository.
|
||||
|
||||
This endpoint creates a new Git repository with the specified name.
|
||||
If the repository already exists, it returns a status message indicating so.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to be created.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status message and HTTP status code.
|
||||
- 200: If the repository already exists.
|
||||
- 201: If the repository is successfully created.
|
||||
"""
|
||||
data = request.json
|
||||
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
repo = data["name"]
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if os.path.isdir(repo_path):
|
||||
log.error("Can't create repository %s, already exists at %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path})
|
||||
return {"status": "Repository already exists"}, 200
|
||||
|
||||
|
||||
installer = OpengnsysGitInstaller()
|
||||
installer.add_forgejo_repo(repo)
|
||||
|
||||
#installer.init_git_repo(repo + ".git")
|
||||
|
||||
log.info("Repository %s created", repo, extra = {"repository" : repo})
|
||||
return {"status": "Repository created"}, 201
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/sync')
|
||||
class GitRepoSync(Resource):
|
||||
def post(self, repo):
|
||||
"""
|
||||
Synchronize a repository with a remote repository.
|
||||
|
||||
This endpoint triggers the synchronization process for a specified repository.
|
||||
It expects a JSON payload with the remote repository details.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to be synchronized.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response indicating the status of the synchronization process.
|
||||
- 200: If the synchronization process has started successfully.
|
||||
- 400: If the request payload is missing or invalid.
|
||||
- 404: If the specified repository is not found.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't sync repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
data = request.json
|
||||
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
if not "remote_repository" in data:
|
||||
log.error("Can't create repository, parameter 'remote_repository' missing")
|
||||
return {"error" : "Parameter 'remote_repository' missing"}, 400
|
||||
|
||||
|
||||
future = executor.submit(do_repo_sync, repo, data)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting synchronization of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/backup')
|
||||
class GitRepoBackup(Resource):
|
||||
def backup_repository(self, repo):
|
||||
"""
|
||||
Backup a specified repository.
|
||||
|
||||
Endpoint: POST /repositories/<repo>/backup
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to back up.
|
||||
|
||||
Request Body (JSON):
|
||||
ssh_port (int, optional): The SSH port to use for the backup. Defaults to 22.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response indicating the status of the backup operation.
|
||||
- If the repository is not found, returns a 404 error with a message.
|
||||
- If the request body is missing, returns a 400 error with a message.
|
||||
- If the backup process starts successfully, returns a 200 status with the task ID.
|
||||
|
||||
Notes:
|
||||
- The repository path is constructed by appending ".git" to the repository name.
|
||||
- The backup operation is performed asynchronously using a thread pool executor.
|
||||
- The task ID of the backup operation is generated using UUID and stored in a global tasks dictionary.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't backup repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
data = request.json
|
||||
if data is None:
|
||||
log.error("Can't create repository, JSON post data missing")
|
||||
return {"error" : "Parameters missing"}, 400
|
||||
|
||||
|
||||
if not "ssh_port" in data:
|
||||
data["ssh_port"] = 22
|
||||
|
||||
|
||||
future = executor.submit(do_repo_backup, repo, data)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting backup of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
@git_ns.route('/repositories/<repo>/compact', methods=['POST'])
|
||||
class GitRepoCompact(Resource):
|
||||
def post(self, repo):
|
||||
"""
|
||||
Initiates a garbage collection (GC) process for a specified Git repository.
|
||||
|
||||
This endpoint triggers an asynchronous GC task for the given repository.
|
||||
The task is submitted to an executor, and a unique task ID is generated
|
||||
and returned to the client.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to perform GC on.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing the status of the request and
|
||||
a unique task ID if the repository is found, or an error
|
||||
message if the repository is not found.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't compact repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
future = executor.submit(do_repo_gc, repo)
|
||||
task_id = add_task(future)
|
||||
|
||||
log.info("Starting compaction of repository %s, task %s", repo, task_id, extra = {"repository" : repo, "task_id" : task_id})
|
||||
return {"status": "started", "task_id" : task_id}, 200
|
||||
|
||||
|
||||
@git_ns.route('/tasks/<task_id>/status')
|
||||
class GitTaskStatus(Resource):
|
||||
def get(self, task_id):
|
||||
"""
|
||||
Endpoint to check the status of a specific task.
|
||||
|
||||
Args:
|
||||
task_id (str): The unique identifier of the task.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing the status of the task.
|
||||
- If the task is not found, returns a 404 error with an error message.
|
||||
- If the task is completed, returns a 200 status with the result.
|
||||
- If the task is still in progress, returns a 202 status indicating the task is in progress.
|
||||
"""
|
||||
if not task_id in tasks:
|
||||
log.error("Task %s was not found", task_id, extra = {"task_id" : task_id})
|
||||
return {"error": "Task not found"}, 404
|
||||
|
||||
future = tasks[task_id]["future"]
|
||||
|
||||
try:
|
||||
if future.done():
|
||||
result = future.result()
|
||||
log.info("Returning completion of task %s", task_id, extra = {"task_id" : task_id})
|
||||
return {"status" : "completed", "result" : result}, 200
|
||||
else:
|
||||
log.info("Task %s is still in progress", task_id, extra = {"task_id" : task_id})
|
||||
return {"status" : "in progress"}, 202
|
||||
except Exception as e:
|
||||
errid = uuid.uuid4().hex
|
||||
|
||||
|
||||
log.error("Task %s failed with exception %s, UUID %s", task_id, traceback.format_exception(e), errid, extra = {"task_id" : task_id, "exception" : traceback.format_exception(e), "error_id" : errid})
|
||||
return {"status" : "internal error", "error_id" : errid }, 500
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>', methods=['DELETE'])
|
||||
class GitRepo(Resource):
|
||||
def delete(self, repo):
|
||||
"""
|
||||
Deletes a Git repository.
|
||||
|
||||
This endpoint deletes a Git repository specified by the `repo` parameter.
|
||||
If the repository does not exist, it returns a 404 error with a message
|
||||
indicating that the repository was not found. If the repository is successfully
|
||||
deleted, it returns a 200 status with a message indicating that the repository
|
||||
was deleted.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository to delete.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status message and the appropriate HTTP status code.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't delete repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
|
||||
shutil.rmtree(repo_path)
|
||||
log.info("Deleted repository %s", repo, extra = {"repository" : repo})
|
||||
return {"status": "Repository deleted"}, 200
|
||||
|
||||
|
||||
|
||||
|
||||
@git_ns.route('/repositories/<repo>/branches')
|
||||
class GitRepoBranches(Resource):
|
||||
def get(self, repo):
|
||||
"""
|
||||
Retrieve the list of branches for a given repository.
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of branch names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "branches" key containing a list of branch names.
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" if the repository does not exist.
|
||||
"""
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get branches of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
branches = []
|
||||
for branch in git_repo.branches:
|
||||
branches = branches + [branch.name]
|
||||
|
||||
log.info("Returning %i branches", len(branches))
|
||||
return {
|
||||
"branches": branches
|
||||
}
|
||||
|
||||
@git_ns.route('/repositories/<repo>/branches/<branch>')
|
||||
class GitRepoBranchesDeleter(Resource):
|
||||
def delete(self, repo, branch):
|
||||
"""Delete a given branch in a given repository
|
||||
|
||||
Args:
|
||||
repo (str): The name of the repository.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response containing a list of branch names or an error message if the repository is not found.
|
||||
- 200: A JSON object with a "status" key containing "deleted"
|
||||
- 404: A JSON object with an "error" key containing the message "Repository not found" or "Branch not found"
|
||||
"""
|
||||
|
||||
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
|
||||
if not os.path.isdir(repo_path):
|
||||
log.error("Can't get branches of repository repository %s, not found. Looked in %s", repo, repo_path, extra = {"repository" : repo, "path" : repo_path })
|
||||
return {"error": "Repository not found"}, 404
|
||||
|
||||
git_repo = git.Repo(repo_path)
|
||||
git_repo.git.config('--global', '--add', 'safe.directory', repo_path)
|
||||
|
||||
|
||||
if not branch in git_repo.branches:
|
||||
log.error("Can't delete branch %s, not found in repository %s", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"error": "Branch not found"}, 404
|
||||
|
||||
git_repo.delete_head(branch)
|
||||
log.info("Branch %s of repository %s deleted", branch, repo, extra = {"repository" : repo, "branch" : branch})
|
||||
return {"status": "deleted"}, 200
|
||||
|
||||
|
||||
@git_ns.route('/health')
|
||||
class GitHealth(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Health check endpoint.
|
||||
|
||||
This endpoint returns a JSON response indicating the health status of the application.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with a status key set to "OK". Currently it always returns
|
||||
a successful value, but this endpoint can still be used to check that the API is
|
||||
active and functional.
|
||||
|
||||
"""
|
||||
log.info("Health check endpoint called")
|
||||
return {
|
||||
"status": "OK"
|
||||
}
|
||||
|
||||
@git_ns.route('/status')
|
||||
class GitStatus(Resource):
|
||||
def get(self):
|
||||
"""
|
||||
Status check endpoint.
|
||||
|
||||
This endpoint returns a JSON response indicating the status of the application.
|
||||
|
||||
Returns:
|
||||
Response: A JSON response with status information
|
||||
|
||||
"""
|
||||
log.info("Status endpoint called")
|
||||
|
||||
return {
|
||||
"uptime" : time.time() - start_time,
|
||||
"active_tasks" : len(tasks)
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -0,0 +1,43 @@
|
|||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Source: <url://example.com>
|
||||
Upstream-Name: ogboot
|
||||
Upstream-Contact: <preferred name and address to reach the upstream project>
|
||||
|
||||
Files:
|
||||
*
|
||||
Copyright:
|
||||
<years> <put author's name and email here>
|
||||
<years> <likewise for another author>
|
||||
License: GPL-3.0+
|
||||
|
||||
Files:
|
||||
debian/*
|
||||
Copyright:
|
||||
2025 vagrant <vagrant@build>
|
||||
License: GPL-3.0+
|
||||
|
||||
License: GPL-3.0+
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
.
|
||||
This package is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
.
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
Comment:
|
||||
On Debian systems, the complete text of the GNU General
|
||||
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
|
||||
|
||||
# Please also look if there are files or directories which have a
|
||||
# different copyright/license attached and list them here.
|
||||
# Please avoid picking licenses with terms that are more restrictive than the
|
||||
# packaged work, as it may make Debian's contributions unacceptable upstream.
|
||||
#
|
||||
# If you need, there are some extra license texts available in two places:
|
||||
# /usr/share/debhelper/dh_make/licenses/
|
||||
# /usr/share/common-licenses/
|
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
if [ ! -f "/etc/apt/sources.list.d/opengnsys.sources" ] ; then
|
||||
|
||||
cat > /etc/apt/sources.list.d/opengnsys.sources <<HERE
|
||||
Types: deb
|
||||
URIs: https://ognproject.evlt.uma.es/debian-opengnsys/
|
||||
Suites: noble
|
||||
Components: main
|
||||
Signed-By:
|
||||
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
.
|
||||
mDMEZzx/SxYJKwYBBAHaRw8BAQdAa83CuAJ5/+7Pn9LHT/k34EAGpx5FnT/ExHSj
|
||||
XZG1JES0Ik9wZW5HbnN5cyA8b3Blbmduc3lzQG9wZW5nbnN5cy5lcz6ImQQTFgoA
|
||||
QRYhBC+J38Xsso227ZbDVt2S5xJQRhKDBQJnPH9LAhsDBQkFo5qABQsJCAcCAiIC
|
||||
BhUKCQgLAgQWAgMBAh4HAheAAAoJEN2S5xJQRhKDW/MBAO6swnpwdrbm48ypMyPh
|
||||
NboxvF7rCqBqHWwRHvkvrq7pAP9zd98r7z2AvqVXZxnaCsLTUNMEL12+DVZAUZ1G
|
||||
EquRBbg4BGc8f0sSCisGAQQBl1UBBQEBB0B6D6tkrwXSHi7ebGYsiMPntqwdkQ/S
|
||||
84SFTlSxRqdXfgMBCAeIfgQYFgoAJhYhBC+J38Xsso227ZbDVt2S5xJQRhKDBQJn
|
||||
PH9LAhsMBQkFo5qAAAoJEN2S5xJQRhKDJ+cBAM9jYbeq5VXkHLfODeVztgSXnSUe
|
||||
yklJ18oQmpeK5eWeAQDKYk/P0R+1ZJDItxkeP6pw62bCDYGQDvdDGPMAaIT6CA==
|
||||
=xcNc
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
HERE
|
||||
fi
|
||||
|
||||
|
||||
apt update
|
||||
apt install -y python3-git opengnsys-libarchive-c python3-termcolor python3-requests python3-tqdm bsdextrautils python3-paramiko python3-aniso8601 opengnsys-flask-restx opengnsys-flask-executor python3-flask python3-psutil
|
|
@ -0,0 +1,11 @@
|
|||
[Service]
|
||||
RestartSec=10s
|
||||
Type=simple
|
||||
User=oggit
|
||||
Group=oggit
|
||||
WorkingDirectory=/opt/opengnsys/ogrepository/oggit/api/
|
||||
ExecStart=/usr/bin/gunicorn -w 4 -b 0.0.0.0:8006 api_server:app
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
|
@ -0,0 +1,122 @@
|
|||
# GitLib
|
||||
|
||||
The `gitlib.py` is a Python library also usable as a command-line program for testing purposes.
|
||||
|
||||
It contains functions for managing git, and the command-line interface allows executing them without needing to write a program that uses the library.
|
||||
|
||||
## Requirements
|
||||
|
||||
Gitlib is designed to work within an existing OpenGnsys environment. It invokes some OpenGnsys commands internally and reads the parameters passed to the kernel in oglive.
|
||||
|
||||
Therefore, it will not work correctly outside of an oglive environment.
|
||||
|
||||
## Installing Python dependencies
|
||||
|
||||
The code conversion to Python 3 currently requires the packages specified in `requirements.txt`.
|
||||
|
||||
The `venv` module (https://docs.python.org/3/library/venv.html) is used to install Python dependencies, creating an environment isolated from the system.
|
||||
|
||||
**Note:** Ubuntu 24.04 includes most of the required dependencies as packages, but there is no `blkid` package, so it must be installed using pip within a virtual environment.
|
||||
|
||||
Run the following commands:
|
||||
|
||||
```bash
|
||||
sudo apt install -y python3 libarchive-dev libblkid-dev pkg-config libacl1-dev
|
||||
python3 -m venv venvog
|
||||
. venvog/bin/activate
|
||||
python3 -m pip install --upgrade pip
|
||||
pip3 install -r requirements.txt
|
||||
```
|
||||
|
||||
# Usage
|
||||
|
||||
Run with:
|
||||
|
||||
```bash
|
||||
# . venvog/bin/activate
|
||||
# ./gitlib.py
|
||||
```
|
||||
|
||||
In command-line mode, help can be displayed with:
|
||||
|
||||
```bash
|
||||
./gitlib.py --help
|
||||
```
|
||||
|
||||
**Note:** Execute as the `root` user, as `sudo` clears the environment variable changes made by venv. This will likely result in a Python module not found error or program failure due to outdated dependencies.
|
||||
|
||||
**Note:** Commands starting with `--test` exist for internal testing. They are temporary and meant to test specific parts of the code. These may require specific conditions to work and will be removed upon completion of development.
|
||||
|
||||
## Initialize a repository:
|
||||
|
||||
```bash
|
||||
./gitlib.py --init-repo-from /dev/sda2 --repo linux
|
||||
```
|
||||
|
||||
This initializes the 'linux' repository with the content of /mnt/sda2.
|
||||
|
||||
`--repo` specifies the name of one of the repositories configured during the git installation (see git installer).
|
||||
|
||||
The repository is uploaded to the ogrepository, obtained from the boot parameter passed to the kernel.
|
||||
|
||||
## Clone a repository:
|
||||
|
||||
```bash
|
||||
./gitlib.py --clone-repo-to /dev/sda2 --boot-device /dev/sda --repo linux
|
||||
```
|
||||
|
||||
This clones a repository from the ogrepository. The target is a physical device that will be formatted with the necessary file system.
|
||||
|
||||
`--boot-device` specifies the boot device where the bootloader (GRUB or similar) will be installed.
|
||||
|
||||
`--repo` is the repository name contained in ogrepository.
|
||||
|
||||
# Special Considerations for Windows
|
||||
|
||||
## Cloning
|
||||
|
||||
* Windows must be completely shut down, not hibernated. See: https://learn.microsoft.com/en-us/troubleshoot/windows-client/setup-upgrade-and-drivers/disable-and-re-enable-hibernation
|
||||
* Windows must be cleanly shut down using "Shut Down". Gitlib may fail to mount a disk from an improperly shut down system. If so, boot Windows again and shut it down properly.
|
||||
* Disk encryption (BitLocker) cannot be used.
|
||||
|
||||
## Restoration
|
||||
|
||||
Windows uses a structure called BCD (https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/bcd-system-store-settings-for-uefi?view=windows-11) to store boot configuration.
|
||||
|
||||
This structure can vary depending on the machine where it is deployed. For this reason, gitlib supports storing multiple versions of the BCD internally and selecting the one corresponding to a specific machine.
|
||||
|
||||
# Documentation
|
||||
|
||||
Python documentation can be generated using utilities such as `pdoc3` (other alternatives are also possible):
|
||||
|
||||
```bash
|
||||
# Install pdoc3
|
||||
pip install --user pdoc3
|
||||
|
||||
# Generate documentation
|
||||
pdoc3 --force --html opengnsys_git_installer.py
|
||||
```
|
||||
|
||||
# Functionality
|
||||
|
||||
## Metadata
|
||||
|
||||
Git cannot store data about extended attributes, sockets, or other special file types. Gitlib stores these in `.opengnsys-metadata` at the root of the repository.
|
||||
|
||||
The data is saved in `jsonl` files, a structure with one JSON object per line. This facilitates partial applications by applying only the necessary lines.
|
||||
|
||||
The following files are included:
|
||||
|
||||
* `acls.jsonl`: ACLs
|
||||
* `empty_directories.jsonl`: Empty directories, as Git cannot store them
|
||||
* `filesystems.json`: Information about file systems: types, sizes, UUIDs
|
||||
* `gitignores.jsonl`: List of .gitignore files (renamed to avoid interfering with Git)
|
||||
* `metadata.json`: General metadata about the repository
|
||||
* `special_files.jsonl`: Special files like sockets
|
||||
* `xattrs.jsonl`: Extended attributes
|
||||
* `renamed.jsonl`: Files renamed to avoid interfering with Git
|
||||
* `unix_permissions.jsonl`: UNIX permissions (not precisely stored by Git)
|
||||
* `ntfs_secaudit.txt`: NTFS security data
|
||||
* `efi_data`: Copy of the EFI (ESP) partition
|
||||
* `efi_data.(id)`: EFI partition copy corresponding to a specific machine
|
||||
* `efi_data.(name)`: EFI partition copy corresponding to a name specified by the administrator.
|
|
@ -0,0 +1,149 @@
|
|||
# GitLib
|
||||
|
||||
La `gitlib.py` es una librería de Python también usable como programa de línea
|
||||
de comandos para pruebas.
|
||||
|
||||
Contiene las funciones de gestión de git, y la parte de línea de comandos permite ejecutarlas sin necesitar escribir un programa que use la librería.
|
||||
|
||||
|
||||
## Requisitos
|
||||
|
||||
La gitlib esta diseñada para funcionar dentro de un entorno opengnsys existente. Invoca algunos de los comandos de opengnsys internamente, y lee los parámetros pasados al kernel en el oglive.
|
||||
|
||||
Por lo tanto, no va a funcionar correctamente fuera de un entorno oglive.
|
||||
|
||||
## 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.
|
||||
|
||||
**Nota:** Ubuntu 24.04 tiene la mayoría de las dependencias necesarias como paquetes, pero no hay paquete de `blkid`, por lo cual es necesario usar pip y un virtualenv.
|
||||
|
||||
Ejecutar:
|
||||
|
||||
sudo apt install -y python3 libarchive-dev libblkid-dev pkg-config libacl1-dev
|
||||
python3 -m venv venvog
|
||||
. venvog/bin/activate
|
||||
python3 -m pip install --upgrade pip
|
||||
pip3 install -r requirements.txt
|
||||
|
||||
|
||||
# Uso
|
||||
|
||||
Ejecutar con:
|
||||
|
||||
# . venvog/bin/activate
|
||||
# ./gitlib.py
|
||||
|
||||
En modo de linea de comando, hay ayuda que se puede ver con:
|
||||
|
||||
./gitlib.py --help
|
||||
|
||||
|
||||
**Nota:** Ejecutar como usuario `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.
|
||||
|
||||
**Nota:** Los comandos que comienzan por `--test` existen para hacer pruebas internas, y existen temporalmente para probar partes especificas del código. Es posible que necesiten condiciones especificas para funcionar, y van a eliminarse al completarse el desarrollo.
|
||||
|
||||
|
||||
## Inicializar un repositorio:
|
||||
|
||||
./gitlib.py --init-repo-from /dev/sda2 --repo linux
|
||||
|
||||
|
||||
Esto inicializa el repositorio 'linux' con el contenido /mnt/sda2.
|
||||
|
||||
`--repo` especifica el nombre de uno de los repositorios fijados durante la instalación de git (ver git installer).
|
||||
|
||||
El repositorio de sube al ogrepository, que se obtiene del parámetro de arranque pasado al kernel.
|
||||
|
||||
## Clonar un repositorio:
|
||||
|
||||
./gitlib.py --clone-repo-to /dev/sda2 --boot-device /dev/sda --repo linux
|
||||
|
||||
Esto clona un repositorio del ogrepository. El destino es un dispositivo físico que se va a formatear con el sistema de archivos necesario.
|
||||
|
||||
`--boot-device` especifica el dispositivo de arranque donde se va a instalar el bootloader (GRUB o similar)
|
||||
|
||||
`--repo` es el nombre de repositorio contenido en ogrepository.
|
||||
|
||||
# Consideraciones especiales para Windows
|
||||
|
||||
## Clonación
|
||||
|
||||
* Windows debe haber sido apagado completamente, sin hibernar. Ver https://learn.microsoft.com/en-us/troubleshoot/windows-client/setup-upgrade-and-drivers/disable-and-re-enable-hibernation
|
||||
* Windows debe haber sido apagado limpiamente, usando "Apagar sistema". Es posible que gitlib no pueda montar un disco de un sistema apagado incorrectamente. En ese caso hay que volver a iniciar Windows, y apagarlo.
|
||||
* No se puede usar cifrado de disco (Bitlocker)
|
||||
|
||||
## Restauración
|
||||
|
||||
Windows usa una estructura llamada BCD (https://learn.microsoft.com/en-us/windows-hardware/manufacture/desktop/bcd-system-store-settings-for-uefi?view=windows-11) para almacenar la configuración de arranque.
|
||||
|
||||
La estructura puede variar dependiendo en que maquina se despliegue, por esto gitlib soporta almacenar internamente multiples versiones del BCD, y elegir el correspondiente a una maquina especifica.
|
||||
|
||||
## Identificadores de disco
|
||||
|
||||
El arranque de Windows dependiendo de como esté configurado por Windows puede referirse
|
||||
a UUIDs de particiones y discos cuando se usa particionado GPT.
|
||||
|
||||
El código actual conserva los UUIDs y los restaura al clonar.
|
||||
|
||||
## BCDs específicos
|
||||
|
||||
Los datos de arranque de Windows se guardan en `.opengsnys-metadata/efi_data`. Es posible incluir versiones adicionales en caso necesario. Se hace creando un directorio adicional con el nombre `efi_data.(id)`, donde id es un número de serie obtenido con el comando `/usr/sbin/dmidecode -s system-uuid`.
|
||||
|
||||
Por ejemplo:
|
||||
|
||||
```
|
||||
# Obtener ID único del equipo
|
||||
|
||||
dmidecode -s system-uuid
|
||||
a64cc65b-12a6-42ef-8182-5ae4832e9f19
|
||||
|
||||
# Copiar la partición EFI al directorio correspondiente a esa máquina particular
|
||||
mkdir /mnt/sda3/.opengnsys-metadata/efi_data.a64cc65b-12a6-42ef-8182-5ae4832e9f19
|
||||
cp -Rdpv /mnt/sda1/* /mnt/sda3/.opengnsys-metadata/efi_data.a64cc65b-12a6-42ef-8182-5ae4832e9f19
|
||||
|
||||
# commit
|
||||
```
|
||||
|
||||
Con esto, al desplegar el repo, para la máquina a64cc65b-12a6-42ef-8182-5ae4832e9f19 se va a usar su propia configuración de arranque, en vez de la general.
|
||||
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
# Funcionamiento
|
||||
|
||||
|
||||
|
||||
## Metadatos
|
||||
|
||||
Git no es capaz de almacenar datos de atributos extendidos, sockets y otros tipos de archivos especiales. El gitlib los almacena en .opengnsys-metadata en
|
||||
el raíz del repositorio.
|
||||
|
||||
Los datos se guardan en archivos de tipo `jsonl`, una estructura de JSON por linea. Esto es para facilitar aplicaciones parciales solo aplicando el efecto de las lineas necesarias.
|
||||
|
||||
Existen estos archivos:
|
||||
|
||||
* `acls.jsonl`: ACLs
|
||||
* `empty_directories.jsonl`: Directorios vacíos, ya que Git no es capaz de guardarlos
|
||||
* `filesystems.json`: Información sobre sistemas de archivos: tipos, tamaños, UUIDs
|
||||
* `gitignores.jsonl`: Lista de archivos .gitignore (los renombramos para que no interfieran con git)
|
||||
* `metadata.json`: Metadatos generales acerca del repositorio
|
||||
* `special_files.jsonl`: Archivos especiales como sockets
|
||||
* `xattrs.jsonl`: Atributos extendidos
|
||||
* `renamed.jsonl`: Archivos renombrados para no interferir con Git
|
||||
* `unix_permissions.jsonl`: Permisos UNIX (Git no los almacena exactamente)
|
||||
* `ntfs_secaudit.txt`: Datos de seguridad de NTFS
|
||||
* `efi_data`: Copia de la partición EFI (ESP)
|
||||
* `efi_data.(id)`: Copia de la partición EFI correspondiente a un equipo especifico.
|
||||
* `efi_data.(nombre)`: Copia de la partición EFI correspondiente a un nombre especificado por el administrador.
|
|
@ -0,0 +1,25 @@
|
|||
# Instalar de Admin
|
||||
|
||||
. venv/bin/activate
|
||||
./opengnsys_git_installer.py
|
||||
|
||||
# Inicializar el repo a partir de los datos de una maquina modelo:
|
||||
|
||||
Ejecutar en oglive corriendo en la maquina modelo
|
||||
|
||||
. venv/bin/activate
|
||||
./gitlib.py --init-repo-from /dev/sda2 --repo linux
|
||||
|
||||
|
||||
# Usar git para desplegar sobre una maquina nueva:
|
||||
|
||||
|
||||
Ejecutar en oglive corriendo en la maquina de destino.
|
||||
|
||||
Preparar el disco creando partición boot/EFI y partición de datos.
|
||||
|
||||
|
||||
. venv/bin/activate
|
||||
./gitlib.py --clone-repo-to /dev/sda2 --repo linux --boot-device /dev/sda
|
||||
|
||||
|
|
@ -0,0 +1,345 @@
|
|||
#!/usr/bin/env python3
|
||||
import hivex
|
||||
import argparse
|
||||
import struct
|
||||
|
||||
from hivex import Hivex
|
||||
from hivex.hive_types import *
|
||||
|
||||
|
||||
# Docs:
|
||||
#
|
||||
# https://www.geoffchappell.com/notes/windows/boot/bcd/objects.htm
|
||||
# https://learn.microsoft.com/en-us/previous-versions/windows/desktop/bcd/bcdbootmgrelementtypes
|
||||
|
||||
#print(f"Root: {root}")
|
||||
|
||||
|
||||
BCD_Enumerations = {
|
||||
"BcdLibraryDevice_ApplicationDevice" : 0x11000001,
|
||||
"BcdLibraryString_ApplicationPath" : 0x12000002,
|
||||
"BcdLibraryString_Description" : 0x12000004,
|
||||
"BcdLibraryString_PreferredLocale" : 0x12000005,
|
||||
"BcdLibraryObjectList_InheritedObjects" : 0x14000006,
|
||||
"BcdLibraryInteger_TruncatePhysicalMemory" : 0x15000007,
|
||||
"BcdLibraryObjectList_RecoverySequence" : 0x14000008,
|
||||
"BcdLibraryBoolean_AutoRecoveryEnabled" : 0x16000009,
|
||||
"BcdLibraryIntegerList_BadMemoryList" : 0x1700000a,
|
||||
"BcdLibraryBoolean_AllowBadMemoryAccess" : 0x1600000b,
|
||||
"BcdLibraryInteger_FirstMegabytePolicy" : 0x1500000c,
|
||||
"BcdLibraryInteger_RelocatePhysicalMemory" : 0x1500000D,
|
||||
"BcdLibraryInteger_AvoidLowPhysicalMemory" : 0x1500000E,
|
||||
"BcdLibraryBoolean_DebuggerEnabled" : 0x16000010,
|
||||
"BcdLibraryInteger_DebuggerType" : 0x15000011,
|
||||
"BcdLibraryInteger_SerialDebuggerPortAddress" : 0x15000012,
|
||||
"BcdLibraryInteger_SerialDebuggerPort" : 0x15000013,
|
||||
"BcdLibraryInteger_SerialDebuggerBaudRate" : 0x15000014,
|
||||
"BcdLibraryInteger_1394DebuggerChannel" : 0x15000015,
|
||||
"BcdLibraryString_UsbDebuggerTargetName" : 0x12000016,
|
||||
"BcdLibraryBoolean_DebuggerIgnoreUsermodeExceptions" : 0x16000017,
|
||||
"BcdLibraryInteger_DebuggerStartPolicy" : 0x15000018,
|
||||
"BcdLibraryString_DebuggerBusParameters" : 0x12000019,
|
||||
"BcdLibraryInteger_DebuggerNetHostIP" : 0x1500001A,
|
||||
"BcdLibraryInteger_DebuggerNetPort" : 0x1500001B,
|
||||
"BcdLibraryBoolean_DebuggerNetDhcp" : 0x1600001C,
|
||||
"BcdLibraryString_DebuggerNetKey" : 0x1200001D,
|
||||
"BcdLibraryBoolean_EmsEnabled" : 0x16000020,
|
||||
"BcdLibraryInteger_EmsPort" : 0x15000022,
|
||||
"BcdLibraryInteger_EmsBaudRate" : 0x15000023,
|
||||
"BcdLibraryString_LoadOptionsString" : 0x12000030,
|
||||
"BcdLibraryBoolean_DisplayAdvancedOptions" : 0x16000040,
|
||||
"BcdLibraryBoolean_DisplayOptionsEdit" : 0x16000041,
|
||||
"BcdLibraryDevice_BsdLogDevice" : 0x11000043,
|
||||
"BcdLibraryString_BsdLogPath" : 0x12000044,
|
||||
"BcdLibraryBoolean_GraphicsModeDisabled" : 0x16000046,
|
||||
"BcdLibraryInteger_ConfigAccessPolicy" : 0x15000047,
|
||||
"BcdLibraryBoolean_DisableIntegrityChecks" : 0x16000048,
|
||||
"BcdLibraryBoolean_AllowPrereleaseSignatures" : 0x16000049,
|
||||
"BcdLibraryString_FontPath" : 0x1200004A,
|
||||
"BcdLibraryInteger_SiPolicy" : 0x1500004B,
|
||||
"BcdLibraryInteger_FveBandId" : 0x1500004C,
|
||||
"BcdLibraryBoolean_ConsoleExtendedInput" : 0x16000050,
|
||||
"BcdLibraryInteger_GraphicsResolution" : 0x15000052,
|
||||
"BcdLibraryBoolean_RestartOnFailure" : 0x16000053,
|
||||
"BcdLibraryBoolean_GraphicsForceHighestMode" : 0x16000054,
|
||||
"BcdLibraryBoolean_IsolatedExecutionContext" : 0x16000060,
|
||||
"BcdLibraryBoolean_BootUxDisable" : 0x1600006C,
|
||||
"BcdLibraryBoolean_BootShutdownDisabled" : 0x16000074,
|
||||
"BcdLibraryIntegerList_AllowedInMemorySettings" : 0x17000077,
|
||||
"BcdLibraryBoolean_ForceFipsCrypto" : 0x16000079,
|
||||
|
||||
|
||||
"BcdBootMgrObjectList_DisplayOrder" : 0x24000001,
|
||||
"BcdBootMgrObjectList_BootSequence" : 0x24000002,
|
||||
"BcdBootMgrObject_DefaultObject" : 0x23000003,
|
||||
"BcdBootMgrInteger_Timeout" : 0x25000004,
|
||||
"BcdBootMgrBoolean_AttemptResume" : 0x26000005,
|
||||
"BcdBootMgrObject_ResumeObject" : 0x23000006,
|
||||
"BcdBootMgrObjectList_ToolsDisplayOrder" : 0x24000010,
|
||||
"BcdBootMgrBoolean_DisplayBootMenu" : 0x26000020,
|
||||
"BcdBootMgrBoolean_NoErrorDisplay" : 0x26000021,
|
||||
"BcdBootMgrDevice_BcdDevice" : 0x21000022,
|
||||
"BcdBootMgrString_BcdFilePath" : 0x22000023,
|
||||
"BcdBootMgrBoolean_ProcessCustomActionsFirst" : 0x26000028,
|
||||
"BcdBootMgrIntegerList_CustomActionsList" : 0x27000030,
|
||||
"BcdBootMgrBoolean_PersistBootSequence" : 0x26000031,
|
||||
|
||||
"BcdDeviceInteger_RamdiskImageOffset" : 0x35000001,
|
||||
"BcdDeviceInteger_TftpClientPort" : 0x35000002,
|
||||
"BcdDeviceInteger_SdiDevice" : 0x31000003,
|
||||
"BcdDeviceInteger_SdiPath" : 0x32000004,
|
||||
"BcdDeviceInteger_RamdiskImageLength" : 0x35000005,
|
||||
"BcdDeviceBoolean_RamdiskExportAsCd" : 0x36000006,
|
||||
"BcdDeviceInteger_RamdiskTftpBlockSize" : 0x36000007,
|
||||
"BcdDeviceInteger_RamdiskTftpWindowSize" : 0x36000008,
|
||||
"BcdDeviceBoolean_RamdiskMulticastEnabled" : 0x36000009,
|
||||
"BcdDeviceBoolean_RamdiskMulticastTftpFallback" : 0x3600000A,
|
||||
"BcdDeviceBoolean_RamdiskTftpVarWindow" : 0x3600000B,
|
||||
|
||||
"BcdMemDiagInteger_PassCount" : 0x25000001,
|
||||
"BcdMemDiagInteger_FailureCount" : 0x25000003,
|
||||
|
||||
"Reserved1" : 0x21000001,
|
||||
"Reserved2" : 0x22000002,
|
||||
"BcdResumeBoolean_UseCustomSettings" : 0x26000003,
|
||||
"BcdResumeDevice_AssociatedOsDevice" : 0x21000005,
|
||||
"BcdResumeBoolean_DebugOptionEnabled" : 0x26000006,
|
||||
"BcdResumeInteger_BootMenuPolicy" : 0x25000008,
|
||||
|
||||
"BcdOSLoaderDevice_OSDevice" : 0x21000001,
|
||||
"BcdOSLoaderString_SystemRoot" : 0x22000002,
|
||||
"BcdOSLoaderObject_AssociatedResumeObject" : 0x23000003,
|
||||
"BcdOSLoaderBoolean_DetectKernelAndHal" : 0x26000010,
|
||||
"BcdOSLoaderString_KernelPath" : 0x22000011,
|
||||
"BcdOSLoaderString_HalPath" : 0x22000012,
|
||||
"BcdOSLoaderString_DbgTransportPath" : 0x22000013,
|
||||
"BcdOSLoaderInteger_NxPolicy" : 0x25000020,
|
||||
"BcdOSLoaderInteger_PAEPolicy" : 0x25000021,
|
||||
"BcdOSLoaderBoolean_WinPEMode" : 0x26000022,
|
||||
"BcdOSLoaderBoolean_DisableCrashAutoReboot" : 0x26000024,
|
||||
"BcdOSLoaderBoolean_UseLastGoodSettings" : 0x26000025,
|
||||
"BcdOSLoaderBoolean_AllowPrereleaseSignatures" : 0x26000027,
|
||||
"BcdOSLoaderBoolean_NoLowMemory" : 0x26000030,
|
||||
"BcdOSLoaderInteger_RemoveMemory" : 0x25000031,
|
||||
"BcdOSLoaderInteger_IncreaseUserVa" : 0x25000032,
|
||||
"BcdOSLoaderBoolean_UseVgaDriver" : 0x26000040,
|
||||
"BcdOSLoaderBoolean_DisableBootDisplay" : 0x26000041,
|
||||
"BcdOSLoaderBoolean_DisableVesaBios" : 0x26000042,
|
||||
"BcdOSLoaderBoolean_DisableVgaMode" : 0x26000043,
|
||||
"BcdOSLoaderInteger_ClusterModeAddressing" : 0x25000050,
|
||||
"BcdOSLoaderBoolean_UsePhysicalDestination" : 0x26000051,
|
||||
"BcdOSLoaderInteger_RestrictApicCluster" : 0x25000052,
|
||||
"BcdOSLoaderBoolean_UseLegacyApicMode" : 0x26000054,
|
||||
"BcdOSLoaderInteger_X2ApicPolicy" : 0x25000055,
|
||||
"BcdOSLoaderBoolean_UseBootProcessorOnly" : 0x26000060,
|
||||
"BcdOSLoaderInteger_NumberOfProcessors" : 0x25000061,
|
||||
"BcdOSLoaderBoolean_ForceMaximumProcessors" : 0x26000062,
|
||||
"BcdOSLoaderBoolean_ProcessorConfigurationFlags" : 0x25000063,
|
||||
"BcdOSLoaderBoolean_MaximizeGroupsCreated" : 0x26000064,
|
||||
"BcdOSLoaderBoolean_ForceGroupAwareness" : 0x26000065,
|
||||
"BcdOSLoaderInteger_GroupSize" : 0x25000066,
|
||||
"BcdOSLoaderInteger_UseFirmwarePciSettings" : 0x26000070,
|
||||
"BcdOSLoaderInteger_MsiPolicy" : 0x25000071,
|
||||
"BcdOSLoaderInteger_SafeBoot" : 0x25000080,
|
||||
"BcdOSLoaderBoolean_SafeBootAlternateShell" : 0x26000081,
|
||||
"BcdOSLoaderBoolean_BootLogInitialization" : 0x26000090,
|
||||
"BcdOSLoaderBoolean_VerboseObjectLoadMode" : 0x26000091,
|
||||
"BcdOSLoaderBoolean_KernelDebuggerEnabled" : 0x260000a0,
|
||||
"BcdOSLoaderBoolean_DebuggerHalBreakpoint" : 0x260000a1,
|
||||
"BcdOSLoaderBoolean_UsePlatformClock" : 0x260000A2,
|
||||
"BcdOSLoaderBoolean_ForceLegacyPlatform" : 0x260000A3,
|
||||
"BcdOSLoaderInteger_TscSyncPolicy" : 0x250000A6,
|
||||
"BcdOSLoaderBoolean_EmsEnabled" : 0x260000b0,
|
||||
"BcdOSLoaderInteger_DriverLoadFailurePolicy" : 0x250000c1,
|
||||
"BcdOSLoaderInteger_BootMenuPolicy" : 0x250000C2,
|
||||
"BcdOSLoaderBoolean_AdvancedOptionsOneTime" : 0x260000C3,
|
||||
"BcdOSLoaderInteger_BootStatusPolicy" : 0x250000E0,
|
||||
"BcdOSLoaderBoolean_DisableElamDrivers" : 0x260000E1,
|
||||
"BcdOSLoaderInteger_HypervisorLaunchType" : 0x250000F0,
|
||||
"BcdOSLoaderBoolean_HypervisorDebuggerEnabled" : 0x260000F2,
|
||||
"BcdOSLoaderInteger_HypervisorDebuggerType" : 0x250000F3,
|
||||
"BcdOSLoaderInteger_HypervisorDebuggerPortNumber" : 0x250000F4,
|
||||
"BcdOSLoaderInteger_HypervisorDebuggerBaudrate" : 0x250000F5,
|
||||
"BcdOSLoaderInteger_HypervisorDebugger1394Channel" : 0x250000F6,
|
||||
"BcdOSLoaderInteger_BootUxPolicy" : 0x250000F7,
|
||||
"BcdOSLoaderString_HypervisorDebuggerBusParams" : 0x220000F9,
|
||||
"BcdOSLoaderInteger_HypervisorNumProc" : 0x250000FA,
|
||||
"BcdOSLoaderInteger_HypervisorRootProcPerNode" : 0x250000FB,
|
||||
"BcdOSLoaderBoolean_HypervisorUseLargeVTlb" : 0x260000FC,
|
||||
"BcdOSLoaderInteger_HypervisorDebuggerNetHostIp" : 0x250000FD,
|
||||
"BcdOSLoaderInteger_HypervisorDebuggerNetHostPort" : 0x250000FE,
|
||||
"BcdOSLoaderInteger_TpmBootEntropyPolicy" : 0x25000100,
|
||||
"BcdOSLoaderString_HypervisorDebuggerNetKey" : 0x22000110,
|
||||
"BcdOSLoaderBoolean_HypervisorDebuggerNetDhcp" : 0x26000114,
|
||||
"BcdOSLoaderInteger_HypervisorIommuPolicy" : 0x25000115,
|
||||
"BcdOSLoaderInteger_XSaveDisable" : 0x2500012b
|
||||
}
|
||||
|
||||
|
||||
def format_value(bcd, bcd_value):
|
||||
|
||||
name = bcd.value_key(bcd_value)
|
||||
(type, length) = bcd.value_type(bcd_value)
|
||||
|
||||
typename = ""
|
||||
str_value = ""
|
||||
if type == REG_SZ:
|
||||
typename = "SZ"
|
||||
str_value = bcd.value_string(bcd_value)
|
||||
elif type == REG_DWORD:
|
||||
typename = "DWORD"
|
||||
dval = bcd.value_dword(bcd_value)
|
||||
|
||||
str_value = hex(dval) + " (" + str(bcd.value_dword(bcd_value)) + ")"
|
||||
elif type == REG_BINARY:
|
||||
typename = "BIN"
|
||||
(length, value) = bcd.value_value(bcd_value)
|
||||
str_value = value.hex()
|
||||
elif type == REG_DWORD_BIG_ENDIAN:
|
||||
typename = "DWORD_BE"
|
||||
elif type == REG_EXPAND_SZ:
|
||||
typename = "EXPAND SZ"
|
||||
elif type == REG_FULL_RESOURCE_DESCRIPTOR:
|
||||
typename = "RES DESC"
|
||||
elif type == REG_LINK:
|
||||
typename = "LINK"
|
||||
elif type == REG_MULTI_SZ:
|
||||
typename = "MULTISZ"
|
||||
(length, str_value) = bcd.value_value(bcd_value)
|
||||
str_value = str_value.decode('utf-16le')
|
||||
str_value = str_value.replace("\0", ";")
|
||||
#value = ";".join("\0".split(value))
|
||||
elif type == REG_NONE:
|
||||
typename = "NONE"
|
||||
elif type == REG_QWORD:
|
||||
typename = "QWORD"
|
||||
elif type == REG_RESOURCE_LIST:
|
||||
typename = "RES LIST"
|
||||
elif type == REG_RESOURCE_REQUIREMENTS_LIST:
|
||||
typename = "REQ LIST"
|
||||
else:
|
||||
typename = str(type)
|
||||
str_value = "???"
|
||||
|
||||
|
||||
return (typename, length, str_value)
|
||||
|
||||
def dump_all(root, depth = 0):
|
||||
|
||||
padding = "\t" * depth
|
||||
|
||||
children = bcd.node_children(root)
|
||||
|
||||
if len(children) > 0:
|
||||
|
||||
for child in children:
|
||||
name = bcd.node_name(child)
|
||||
print(f"{padding}{name}")
|
||||
|
||||
dump_all(child, depth + 1)
|
||||
# print(f"Child: {child}")
|
||||
|
||||
#print(f"Values: {num_vals}")
|
||||
return
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
values = bcd.node_values(root)
|
||||
#print(f"Value list: {values}")
|
||||
|
||||
for v in values:
|
||||
(type_name, length, str_value) = format_value(bcd, v)
|
||||
name = bcd.value_key(v)
|
||||
|
||||
print(f"{padding}{name: <16}: [{type_name: <10}]; ({length: < 4}) {str_value}")
|
||||
|
||||
|
||||
class WindowsBCD:
|
||||
def __init__(self, filename):
|
||||
self.filename = filename
|
||||
self.bcd = Hivex(filename)
|
||||
|
||||
def dump(self, root=None, depth = 0):
|
||||
padding = "\t" * depth
|
||||
|
||||
if root is None:
|
||||
root = self.bcd.root()
|
||||
|
||||
children = self.bcd.node_children(root)
|
||||
|
||||
if len(children) > 0:
|
||||
for child in children:
|
||||
name = self.bcd.node_name(child)
|
||||
print(f"{padding}{name}")
|
||||
|
||||
self.dump(child, depth + 1)
|
||||
return
|
||||
|
||||
values = self.bcd.node_values(root)
|
||||
|
||||
for v in values:
|
||||
(type_name, length, str_value) = format_value(self.bcd, v)
|
||||
name = self.bcd.value_key(v)
|
||||
|
||||
print(f"{padding}{name: <16}: [{type_name: <10}]; ({length: < 4}) {str_value}")
|
||||
|
||||
def list(self):
|
||||
root = self.bcd.root()
|
||||
objects = self.bcd.node_get_child(root, "Objects")
|
||||
|
||||
for child in self.bcd.node_children(objects):
|
||||
entry_id = self.bcd.node_name(child)
|
||||
|
||||
elements = self.bcd.node_get_child(child, "Elements")
|
||||
description_entry = self.bcd.node_get_child(elements, "12000004")
|
||||
|
||||
if description_entry:
|
||||
values = self.bcd.node_values(description_entry)
|
||||
if values:
|
||||
(type_name, length, str_value) = format_value(self.bcd, values[0])
|
||||
print(f"{entry_id}: {str_value}")
|
||||
else:
|
||||
print(f"{entry_id}: [no description value!?]")
|
||||
|
||||
|
||||
appdevice_entry = self.bcd.node_get_child(elements, "11000001")
|
||||
|
||||
if appdevice_entry:
|
||||
values = self.bcd.node_values(appdevice_entry)
|
||||
(length, data) = self.bcd.value_value(values[0])
|
||||
hex = data.hex()
|
||||
print(f"LEN: {length}, HEX: {hex}, RAW: {data}")
|
||||
if len(data) > 10:
|
||||
etype = struct.unpack_from('<I', data, offset = 16)
|
||||
print(f"Type: {etype}")
|
||||
|
||||
|
||||
|
||||
else:
|
||||
print(f"{entry_id}: [no description entry 12000004]")
|
||||
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="Windows BCD parser",
|
||||
description="Parses the BCD",
|
||||
)
|
||||
|
||||
parser.add_argument("--db", type=str, metavar='BCD file', help="Database to use")
|
||||
parser.add_argument("--dump", action='store_true', help="Dumps the specified database")
|
||||
parser.add_argument("--list", action='store_true', help="Lists boot entries in the specified database")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
bcdobj = WindowsBCD(args.db)
|
||||
|
||||
if args.dump:
|
||||
# "/home/vadim/opengnsys/winboot/boot-copy/EFI/Microsoft/Boot/BCD"
|
||||
#bcd = Hivex(args.dump)
|
||||
|
||||
#root = bcd.root()
|
||||
#dump_all(root)
|
||||
bcdobj.dump()
|
||||
elif args.list:
|
||||
bcdobj.list()
|
|
@ -0,0 +1,115 @@
|
|||
|
||||
import logging
|
||||
import subprocess
|
||||
import re
|
||||
|
||||
# pylint: disable=locally-disabled, line-too-long, logging-fstring-interpolation, too-many-lines
|
||||
|
||||
|
||||
class DiskLibrary:
|
||||
def __init__(self):
|
||||
self.logger = logging.getLogger("OpengnsysDiskLibrary")
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
def split_device_partition(self, device):
|
||||
"""
|
||||
Parses a device file like /dev/sda3 into the root device (/dev/sda) and partition number (3)
|
||||
|
||||
Args:
|
||||
device (str): Device in /dev
|
||||
|
||||
Returns:
|
||||
[base_device, partno]
|
||||
"""
|
||||
|
||||
r = re.compile("^(.*?)(\\d+)$")
|
||||
m = r.match(device)
|
||||
disk = m.group(1)
|
||||
partno = int(m.group(2))
|
||||
|
||||
self.logger.debug(f"{device} parsed into disk device {disk}, partition {partno}")
|
||||
return (disk, partno)
|
||||
|
||||
def get_disk_json_data(self, device):
|
||||
"""
|
||||
Returns the partition JSON data dump for the entire disk, even if a partition is passed.
|
||||
|
||||
This is specifically in the format used by sfdisk.
|
||||
|
||||
Args:
|
||||
device (str): Block device, eg, /dev/sda3
|
||||
|
||||
Returns:
|
||||
str: JSON dump produced by sfdisk
|
||||
"""
|
||||
(disk, partno) = self.split_device_partition(device)
|
||||
|
||||
result = subprocess.run(["/usr/sbin/sfdisk", "--json", disk], check=True, capture_output=True, encoding='utf-8')
|
||||
return result.stdout.strip()
|
||||
|
||||
def get_disk_uuid(self, device):
|
||||
"""
|
||||
Returns the UUID of the disk itself, if there's a GPT partition table.
|
||||
|
||||
Args:
|
||||
device (str): Block device, eg, /dev/sda3
|
||||
|
||||
Returns:
|
||||
str: UUID
|
||||
"""
|
||||
(disk, partno) = self.split_device_partition(device)
|
||||
|
||||
result = subprocess.run(["/usr/sbin/sfdisk", "--disk-id", disk], check=True, capture_output=True, encoding='utf-8')
|
||||
return result.stdout.strip()
|
||||
|
||||
def set_disk_uuid(self, device, uuid):
|
||||
|
||||
(disk, partno) = self.split_device_partition(device)
|
||||
|
||||
subprocess.run(["/usr/sbin/sfdisk", "--disk-id", disk, uuid], check=True, encoding='utf-8')
|
||||
|
||||
|
||||
def get_partition_uuid(self, device):
|
||||
"""
|
||||
Returns the UUID of the partition, if there's a GPT partition table.
|
||||
|
||||
Args:
|
||||
device (str): Block device, eg, /dev/sda3
|
||||
|
||||
Returns:
|
||||
str: UUID
|
||||
"""
|
||||
|
||||
(disk, partno) = self.split_device_partition(device)
|
||||
|
||||
result = subprocess.run(["/usr/sbin/sfdisk", "--part-uuid", disk, str(partno)], check=True, capture_output=True, encoding='utf-8')
|
||||
return result.stdout.strip()
|
||||
|
||||
def set_partition_uuid(self, device, uuid):
|
||||
(disk, partno) = self.split_device_partition(device)
|
||||
|
||||
subprocess.run(["/usr/sbin/sfdisk", "--part-uuid", disk, str(partno), uuid], check=True, encoding='utf-8')
|
||||
|
||||
def get_partition_type(self, device):
|
||||
"""
|
||||
Returns the type UUID of the partition, if there's a GPT partition table.
|
||||
|
||||
Args:
|
||||
device (str): Block device, eg, /dev/sda3
|
||||
|
||||
Returns:
|
||||
str: UUID
|
||||
"""
|
||||
|
||||
(disk, partno) = self.split_device_partition(device)
|
||||
|
||||
result = subprocess.run(["/usr/sbin/sfdisk", "--part-type", disk, str(partno)], check=True, capture_output=True, encoding='utf-8')
|
||||
return result.stdout.strip()
|
||||
|
||||
def set_partition_type(self, device, uuid):
|
||||
(disk, partno) = self.split_device_partition(device)
|
||||
|
||||
subprocess.run(["/usr/sbin/sfdisk", "--part-type", disk, str(partno), uuid], check=True, encoding='utf-8')
|
||||
|
||||
|
||||
|
|
@ -0,0 +1,544 @@
|
|||
|
||||
import logging
|
||||
import subprocess
|
||||
import os
|
||||
import json
|
||||
import blkid
|
||||
import time
|
||||
|
||||
from ntfs import *
|
||||
|
||||
|
||||
|
||||
# pylint: disable=locally-disabled, line-too-long, logging-fstring-interpolation, too-many-lines
|
||||
|
||||
|
||||
class FilesystemLibrary:
|
||||
def __init__(self, ntfs_implementation = NTFSImplementation.KERNEL):
|
||||
self.logger = logging.getLogger("OpengnsysFilesystemLibrary")
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
|
||||
self.mounts = {}
|
||||
self.base_mount_path = "/mnt"
|
||||
self.ntfs_implementation = ntfs_implementation
|
||||
|
||||
self.update_mounts()
|
||||
|
||||
def _rmmod(self, module):
|
||||
self.logger.debug("Trying to unload module {module}...")
|
||||
subprocess.run(["/usr/sbin/rmmod", module], check=False)
|
||||
|
||||
def _modprobe(self, module):
|
||||
self.logger.debug("Trying to load module {module}...")
|
||||
subprocess.run(["/usr/sbin/modprobe", module], check=True)
|
||||
|
||||
|
||||
# _parse_mounts
|
||||
def update_mounts(self):
|
||||
"""
|
||||
Update the current mount points by parsing the /proc/mounts file.
|
||||
|
||||
This method reads the /proc/mounts file to gather information about
|
||||
the currently mounted filesystems. It stores this information in a
|
||||
dictionary where the keys are the mount points and the values are
|
||||
dictionaries containing details about each filesystem.
|
||||
|
||||
The details stored for each filesystem include:
|
||||
- device: The device file associated with the filesystem.
|
||||
- mountpoint: The directory where the filesystem is mounted.
|
||||
- type: The type of the filesystem (e.g., ext4, vfat).
|
||||
- options: Mount options associated with the filesystem.
|
||||
- dump_freq: The dump frequency for the filesystem.
|
||||
- passno: The pass number for filesystem checks.
|
||||
|
||||
The method also adds an entry for each mount point with a trailing
|
||||
slash to ensure consistency in accessing the mount points.
|
||||
|
||||
Attributes:
|
||||
mounts (dict): A dictionary where keys are mount points and values
|
||||
are dictionaries containing filesystem details.
|
||||
"""
|
||||
filesystems = {}
|
||||
|
||||
self.logger.debug("Parsing /proc/mounts")
|
||||
|
||||
with open("/proc/mounts", 'r', encoding='utf-8') as mounts:
|
||||
for line in mounts:
|
||||
parts = line.split()
|
||||
data = {}
|
||||
data['device'] = parts[0]
|
||||
data['mountpoint'] = parts[1]
|
||||
data['type'] = parts[2]
|
||||
data['options'] = parts[3]
|
||||
data['dump_freq'] = parts[4]
|
||||
data['passno'] = parts[5]
|
||||
|
||||
filesystems[data["mountpoint"]] = data
|
||||
filesystems[data["mountpoint"] + "/"] = data
|
||||
|
||||
self.mounts = filesystems
|
||||
|
||||
def find_mountpoint(self, device):
|
||||
"""
|
||||
Find the mount point for a given device.
|
||||
|
||||
This method checks if the specified device is currently mounted and returns
|
||||
the corresponding mount point if it is found.
|
||||
|
||||
Args:
|
||||
device (str): The path to the device to check.
|
||||
|
||||
Returns:
|
||||
str or None: The mount point of the device if it is mounted, otherwise None.
|
||||
"""
|
||||
norm = os.path.normpath(device)
|
||||
|
||||
self.logger.debug(f"Checking if {device} is mounted")
|
||||
for mountpoint, mount in self.mounts.items():
|
||||
#self.logger.debug(f"Item: {mount}")
|
||||
#self.logger.debug(f"Checking: " + mount['device'])
|
||||
if mount['device'] == norm:
|
||||
return mountpoint
|
||||
|
||||
return None
|
||||
|
||||
def find_device(self, mountpoint):
|
||||
"""
|
||||
Find the device corresponding to a given mount point.
|
||||
|
||||
Args:
|
||||
mountpoint (str): The mount point to search for.
|
||||
|
||||
Returns:
|
||||
str or None: The device corresponding to the mount point if found,
|
||||
otherwise None.
|
||||
"""
|
||||
self.update_mounts()
|
||||
self.logger.debug("Finding device corresponding to mount point %s", mountpoint)
|
||||
if mountpoint in self.mounts:
|
||||
return self.mounts[mountpoint]['device']
|
||||
else:
|
||||
self.logger.warning("Failed to find mountpoint %s", mountpoint)
|
||||
return None
|
||||
|
||||
def is_mounted(self, device = None, mountpoint = None):
|
||||
def is_mounted(self, device=None, mountpoint=None):
|
||||
"""
|
||||
Check if a device or mountpoint is currently mounted.
|
||||
|
||||
Either checking by device or mountpoint is valid.
|
||||
|
||||
Args:
|
||||
device (str, optional): The device to check if it is mounted.
|
||||
Defaults to None.
|
||||
mountpoint (str, optional): The mountpoint to check if it is mounted.
|
||||
Defaults to None.
|
||||
|
||||
Returns:
|
||||
bool: True if the device is mounted or the mountpoint is in the list
|
||||
of mounts, False otherwise.
|
||||
"""
|
||||
self.update_mounts()
|
||||
if device:
|
||||
return not self.find_mountpoint(device) is None
|
||||
else:
|
||||
return mountpoint in self.mounts
|
||||
|
||||
def unmount(self, device = None, mountpoint = None):
|
||||
def unmount(self, device=None, mountpoint=None):
|
||||
"""
|
||||
Unmounts a filesystem.
|
||||
|
||||
This method unmounts a filesystem either by the device name or the mountpoint.
|
||||
If a device is provided, it finds the corresponding mountpoint and unmounts it.
|
||||
If a mountpoint is provided directly, it unmounts the filesystem at that mountpoint.
|
||||
|
||||
Args:
|
||||
device (str, optional): The device name to unmount. Defaults to None.
|
||||
mountpoint (str, optional): The mountpoint to unmount. Defaults to None.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: If the unmount command fails.
|
||||
|
||||
Logs:
|
||||
Debug information about the unmounting process.
|
||||
"""
|
||||
if device:
|
||||
self.logger.debug("Finding mountpoint of %s", device)
|
||||
mountpoint = self.find_mountpoint(device)
|
||||
|
||||
if not mountpoint is None:
|
||||
self.logger.debug(f"Unmounting {mountpoint}")
|
||||
|
||||
done = False
|
||||
start_time = time.time()
|
||||
timeout = 60
|
||||
|
||||
|
||||
while not done and (time.time() - start_time) < timeout:
|
||||
ret = subprocess.run(["/usr/bin/umount", mountpoint], check=False, capture_output=True, encoding='utf-8')
|
||||
if ret.returncode == 0:
|
||||
done=True
|
||||
else:
|
||||
if "target is busy" in ret.stderr:
|
||||
self.logger.debug("Filesystem busy, waiting. %.1f seconds left", timeout - (time.time() - start_time))
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
raise subprocess.CalledProcessError(ret.returncode, ret.args, output=ret.stdout, stderr=ret.stderr)
|
||||
|
||||
# We've unmounted a new filesystem, update our filesystems list
|
||||
self.update_mounts()
|
||||
else:
|
||||
self.logger.debug(f"{device} is not mounted")
|
||||
|
||||
|
||||
def mount(self, device, mountpoint, filesystem = None):
|
||||
"""
|
||||
Mounts a device to a specified mountpoint.
|
||||
|
||||
Parameters:
|
||||
device (str): The device to be mounted (e.g., '/dev/sda1').
|
||||
mountpoint (str): The directory where the device will be mounted.
|
||||
filesystem (str, optional): The type of filesystem to be used (e.g., 'ext4', 'ntfs'). Defaults to None.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: If the mount command fails.
|
||||
|
||||
Logs:
|
||||
Debug information about the mounting process, including the mount command, return code, stdout, and stderr.
|
||||
|
||||
Side Effects:
|
||||
Creates the mountpoint directory if it does not exist.
|
||||
Updates the internal list of mounted filesystems.
|
||||
"""
|
||||
self.logger.debug(f"Mounting {device} at {mountpoint}")
|
||||
|
||||
if not os.path.exists(mountpoint):
|
||||
self.logger.debug(f"Creating directory {mountpoint}")
|
||||
os.mkdir(mountpoint)
|
||||
|
||||
mount_cmd = ["/usr/bin/mount"]
|
||||
|
||||
if not filesystem is None:
|
||||
mount_cmd = mount_cmd + ["-t", filesystem]
|
||||
|
||||
mount_cmd = mount_cmd + [device, mountpoint]
|
||||
|
||||
self.logger.debug(f"Mount command: {mount_cmd}")
|
||||
result = subprocess.run(mount_cmd, check=True, capture_output = True)
|
||||
|
||||
self.logger.debug(f"retorno: {result.returncode}")
|
||||
self.logger.debug(f"stdout: {result.stdout}")
|
||||
self.logger.debug(f"stderr: {result.stderr}")
|
||||
|
||||
# We've mounted a new filesystem, update our filesystems list
|
||||
self.update_mounts()
|
||||
|
||||
def ensure_mounted(self, device):
|
||||
"""
|
||||
Ensure that the given device is mounted.
|
||||
|
||||
This method attempts to mount the specified device to a path derived from
|
||||
the base mount path and the device's basename. If the device is of type NTFS,
|
||||
it uses the NTFSLibrary to handle the mounting process. For other filesystem
|
||||
types, it uses a generic mount method.
|
||||
|
||||
Args:
|
||||
device (str): The path to the device that needs to be mounted.
|
||||
|
||||
Returns:
|
||||
str: The path where the device is mounted.
|
||||
|
||||
Logs:
|
||||
- Info: When starting the mounting process.
|
||||
- Debug: Various debug information including the mount path, filesystem type,
|
||||
and success message.
|
||||
|
||||
Raises:
|
||||
OSError: If there is an error creating the mount directory or mounting the device.
|
||||
"""
|
||||
|
||||
self.logger.info("Mounting %s", device)
|
||||
|
||||
self.unmount(device = device)
|
||||
path = os.path.join(self.base_mount_path, os.path.basename(device))
|
||||
|
||||
self.logger.debug(f"Will mount repo at {path}")
|
||||
if not os.path.exists(path):
|
||||
os.mkdir(path)
|
||||
|
||||
if self.filesystem_type(device) == "ntfs":
|
||||
self.logger.debug("Handing a NTFS filesystem")
|
||||
|
||||
self._modprobe("ntfs3")
|
||||
self.ntfsfix(device)
|
||||
|
||||
ntfs = NTFSLibrary(self.ntfs_implementation)
|
||||
ntfs.mount_filesystem(device, path)
|
||||
self.update_mounts()
|
||||
|
||||
else:
|
||||
self.logger.debug("Handling a non-NTFS filesystem")
|
||||
self.mount(device, path)
|
||||
|
||||
self.logger.debug("Successfully mounted at %s", path)
|
||||
return path
|
||||
|
||||
|
||||
def filesystem_type(self, device = None, mountpoint = None):
|
||||
"""
|
||||
Determine the filesystem type of a given device or mountpoint.
|
||||
|
||||
Args:
|
||||
device (str, optional): The device to probe. If not provided, the device
|
||||
will be determined based on the mountpoint.
|
||||
mountpoint (str, optional): The mountpoint to find the device for. This
|
||||
is used only if the device is not provided.
|
||||
|
||||
Returns:
|
||||
str: The filesystem type of the device.
|
||||
|
||||
Raises:
|
||||
KeyError: If the filesystem type cannot be determined from the probe.
|
||||
|
||||
Logs:
|
||||
Debug: Logs the process of finding the device, probing the device, and
|
||||
the determined filesystem type.
|
||||
"""
|
||||
|
||||
if device is None:
|
||||
self.logger.debug("Finding device for mountpoint %s", mountpoint)
|
||||
device = self.find_device(mountpoint)
|
||||
|
||||
self.logger.debug(f"Probing {device}")
|
||||
|
||||
pr = blkid.Probe()
|
||||
pr.set_device(device)
|
||||
pr.enable_superblocks(True)
|
||||
pr.set_superblocks_flags(blkid.SUBLKS_TYPE | blkid.SUBLKS_USAGE | blkid.SUBLKS_UUID | blkid.SUBLKS_UUIDRAW | blkid.SUBLKS_LABELRAW)
|
||||
pr.do_safeprobe()
|
||||
|
||||
fstype = pr["TYPE"].decode('utf-8')
|
||||
self.logger.debug(f"FS type is {fstype}")
|
||||
|
||||
return fstype
|
||||
|
||||
def is_filesystem(self, path):
|
||||
"""
|
||||
Check if the given path is a filesystem root.
|
||||
|
||||
Args:
|
||||
path (str): The path to check.
|
||||
|
||||
Returns:
|
||||
bool: True if the path is a filesystem root, False otherwise.
|
||||
"""
|
||||
|
||||
# This is just an alias for better code readability
|
||||
return self.is_mounted(mountpoint = path)
|
||||
|
||||
def create_filesystem(self, fs_type = None, fs_uuid = None, device = None):
|
||||
"""
|
||||
Create a filesystem on the specified device.
|
||||
|
||||
Parameters:
|
||||
fs_type (str): The type of filesystem to create (e.g., 'ntfs', 'ext4', 'xfs', 'btrfs').
|
||||
fs_uuid (str): The UUID to assign to the filesystem.
|
||||
device (str): The device on which to create the filesystem (e.g., '/dev/sda1').
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the filesystem type is not recognized or if the filesystem creation command fails.
|
||||
|
||||
"""
|
||||
|
||||
self.logger.info(f"Creating filesystem {fs_type} with UUID {fs_uuid} in {device}")
|
||||
|
||||
if fs_type == "ntfs" or fs_type == "ntfs3":
|
||||
self.logger.debug("Creating NTFS filesystem")
|
||||
ntfs = NTFSLibrary(self.ntfs_implementation)
|
||||
ntfs.create_filesystem(device, "NTFS")
|
||||
ntfs.modify_uuid(device, fs_uuid)
|
||||
|
||||
else:
|
||||
command = [f"/usr/sbin/mkfs.{fs_type}"]
|
||||
command_args = []
|
||||
|
||||
if fs_type == "ext4" or fs_type == "ext3":
|
||||
command_args = ["-U", fs_uuid, "-F", device]
|
||||
elif fs_type == "xfs":
|
||||
command_args = ["-m", f"uuid={fs_uuid}", "-f", device]
|
||||
elif fs_type == "btrfs":
|
||||
command_args = ["-U", fs_uuid, "-f", device]
|
||||
else:
|
||||
raise RuntimeError(f"Don't know how to create filesystem of type {fs_type}")
|
||||
|
||||
command = command + command_args
|
||||
|
||||
self.logger.debug(f"Creating Linux filesystem of type {fs_type} on {device}, command {command}")
|
||||
result = subprocess.run(command, check = True, capture_output=True)
|
||||
|
||||
self.logger.debug(f"retorno: {result.returncode}")
|
||||
self.logger.debug(f"stdout: {result.stdout}")
|
||||
self.logger.debug(f"stderr: {result.stderr}")
|
||||
|
||||
|
||||
|
||||
def mklostandfound(self, path):
|
||||
"""
|
||||
Recreate the lost+found if necessary.
|
||||
|
||||
When cloning at the root of a filesystem, cleaning the contents
|
||||
removes the lost+found directory. This is a special directory that requires the use of
|
||||
a tool to recreate it.
|
||||
|
||||
It may fail if the filesystem does not need it. We consider this harmless and ignore it.
|
||||
|
||||
The command is entirely skipped on NTFS, as mklost+found may malfunction if run on it,
|
||||
and has no useful purpose.
|
||||
"""
|
||||
if self.is_filesystem(path):
|
||||
if self.filesystem_type(mountpoint=path) == "ntfs":
|
||||
self.logger.debug("Not running mklost+found on NTFS")
|
||||
return
|
||||
|
||||
|
||||
curdir = os.getcwd()
|
||||
result = None
|
||||
|
||||
try:
|
||||
self.logger.debug(f"Re-creating lost+found in {path}")
|
||||
os.chdir(path)
|
||||
result = subprocess.run(["/usr/sbin/mklost+found"], check=True, capture_output=True)
|
||||
except subprocess.SubprocessError as e:
|
||||
self.logger.warning(f"Error running mklost+found: {e}")
|
||||
|
||||
if result:
|
||||
self.logger.debug(f"retorno: {result.returncode}")
|
||||
self.logger.debug(f"stdout: {result.stdout}")
|
||||
self.logger.debug(f"stderr: {result.stderr}")
|
||||
|
||||
os.chdir(curdir)
|
||||
|
||||
def ntfsfix(self, device):
|
||||
"""
|
||||
Run the ntfsfix command on the specified device.
|
||||
|
||||
This method uses the ntfsfix utility to fix common NTFS problems on the given device.
|
||||
|
||||
This allows mounting an unclean NTFS filesystem.
|
||||
|
||||
Args:
|
||||
device (str): The path to the device to be fixed.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: If the ntfsfix command fails.
|
||||
"""
|
||||
self.logger.debug(f"Running ntfsfix on {device}")
|
||||
subprocess.run(["/usr/bin/ntfsfix", "-d", device], check=True)
|
||||
|
||||
|
||||
def unload_ntfs(self):
|
||||
"""
|
||||
Unloads the NTFS filesystem module.
|
||||
|
||||
This is a function added as a result of NTFS kernel module troubleshooting,
|
||||
to try to ensure that NTFS code is only active as long as necessary.
|
||||
|
||||
The module is internally loaded as needed, so there's no load_ntfs function.
|
||||
|
||||
It may be removed in the future.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the module cannot be removed.
|
||||
"""
|
||||
self._rmmod("ntfs3")
|
||||
|
||||
def find_boot_device(self):
|
||||
"""
|
||||
Searches for the EFI boot partition on the system.
|
||||
|
||||
This method scans the system's partitions to locate the EFI boot partition,
|
||||
which is identified by the GUID "C12A7328-F81F-11D2-BA4B-00A0C93EC93B".
|
||||
|
||||
Returns:
|
||||
str: The device node of the EFI partition if found, otherwise None.
|
||||
|
||||
Logs:
|
||||
- Debug messages indicating the progress of the search.
|
||||
- A warning message if the EFI partition is not found.
|
||||
"""
|
||||
disks = []
|
||||
|
||||
self.logger.debug("Looking for EFI partition")
|
||||
with open("/proc/partitions", "r", encoding='utf-8') as partitions_file:
|
||||
line_num=0
|
||||
for line in partitions_file:
|
||||
if line_num >=2:
|
||||
data = line.split()
|
||||
disk = data[3]
|
||||
disks.append(disk)
|
||||
self.logger.debug(f"Disk: {disk}")
|
||||
|
||||
line_num = line_num + 1
|
||||
|
||||
for disk in disks:
|
||||
self.logger.debug("Loading partitions for disk %s", disk)
|
||||
#disk_json_data = subprocess.run(["/usr/sbin/sfdisk", "-J", f"/dev/{disk}"], check=False, capture_output=True)
|
||||
sfdisk_out = subprocess.run(["/usr/sbin/sfdisk", "-J", f"/dev/{disk}"], check=False, capture_output=True)
|
||||
|
||||
if sfdisk_out.returncode == 0:
|
||||
disk_json_data = sfdisk_out.stdout
|
||||
disk_data = json.loads(disk_json_data)
|
||||
|
||||
for part in disk_data["partitiontable"]["partitions"]:
|
||||
self.logger.debug("Checking partition %s", part)
|
||||
if part["type"] == "C12A7328-F81F-11D2-BA4B-00A0C93EC93B":
|
||||
self.logger.debug("EFI partition found at %s", part["node"])
|
||||
return part["node"]
|
||||
else:
|
||||
self.logger.debug("sfdisk returned with code %i, error %s", sfdisk_out.returncode, sfdisk_out.stderr)
|
||||
|
||||
|
||||
self.logger.warning("Failed to find EFI partition!")
|
||||
|
||||
def temp_unmount(self, mountpoint):
|
||||
"""
|
||||
Temporarily unmounts the filesystem at the given mountpoint.
|
||||
|
||||
This method finds the device associated with the specified mountpoint,
|
||||
and returns the information to remount it with temp_remount.
|
||||
|
||||
The purpose of this function is to temporarily unmount a filesystem for
|
||||
actions like fsck, and to mount it back afterwards.
|
||||
|
||||
Args:
|
||||
mountpoint (str): The mountpoint of the filesystem to unmount.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary containing the information needed to remount the filesystem.
|
||||
"""
|
||||
device = self.find_device(mountpoint)
|
||||
fs = self.filesystem_type(mountpoint = mountpoint)
|
||||
|
||||
data = {"mountpoint" : mountpoint, "device" :device, "filesystem" : fs}
|
||||
|
||||
self.logger.debug("Temporarily unmounting device %s, mounted on %s, fs type %s", mountpoint, device, fs)
|
||||
|
||||
self.unmount(mountpoint = mountpoint)
|
||||
return data
|
||||
|
||||
def temp_remount(self, unmount_data):
|
||||
"""
|
||||
Remounts a filesystem unmounted with temp_unmount
|
||||
|
||||
This method remounts a filesystem using the data provided by temp_unmount
|
||||
|
||||
Args:
|
||||
unmount_data (dict): A dictionary containing the data needed to remount the filesystem.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
self.logger.debug("Remounting temporarily unmounted device %s on %s, fs type %s", unmount_data["device"], unmount_data["mountpoint"], unmount_data["filesystem"])
|
||||
self.mount(device = unmount_data["device"], mountpoint=unmount_data["mountpoint"], filesystem=unmount_data["filesystem"])
|
|
@ -0,0 +1,52 @@
|
|||
#!/usr/bin/env python3
|
||||
import unittest
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import tarfile
|
||||
import subprocess
|
||||
from shutil import rmtree
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
parent_dir = str(Path(__file__).parent.parent.absolute())
|
||||
sys.path.append(parent_dir)
|
||||
sys.path.append("/opengnsys/installer")
|
||||
print(parent_dir)
|
||||
|
||||
from gitlib import OpengnsysGitLibrary
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class GitTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.logger = logging.getLogger("OpengnsysTest")
|
||||
self.oggit = OpengnsysGitLibrary()
|
||||
|
||||
self.logger.info("setUp()")
|
||||
if not hasattr(self, 'init_complete'):
|
||||
self.init_complete = True
|
||||
def test_init(self):
|
||||
self.assertIsNotNone(self.oggit)
|
||||
def test_acls(self):
|
||||
self.oggit.ogCreateAcl()
|
||||
|
||||
def test_sync_local(self):
|
||||
# self.oggit.ogSyncLocalGitImage()
|
||||
None
|
||||
|
||||
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")
|
||||
|
||||
unittest.main()
|
||||
|
||||
|
||||
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,22 @@
|
|||
|
||||
|
||||
def parse_kernel_cmdline():
|
||||
"""Parse the kernel arguments to obtain configuration parameters in Oglive
|
||||
|
||||
OpenGnsys passes data in the kernel arguments, for example:
|
||||
[...] group=Aula_virtual ogrepo=192.168.2.1 oglive=192.168.2.1 [...]
|
||||
|
||||
Returns:
|
||||
dict: Dict of configuration parameters and their values.
|
||||
"""
|
||||
params = {}
|
||||
|
||||
with open("/proc/cmdline", encoding='utf-8') as cmdline:
|
||||
line = cmdline.readline()
|
||||
parts = line.split()
|
||||
for part in parts:
|
||||
if "=" in part:
|
||||
key, value = part.split("=")
|
||||
params[key] = value
|
||||
|
||||
return params
|
|
@ -0,0 +1,111 @@
|
|||
|
||||
import logging
|
||||
import subprocess
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class NTFSImplementation(Enum):
|
||||
KERNEL = 1
|
||||
NTFS3G = 2
|
||||
|
||||
|
||||
class NTFSLibrary:
|
||||
"""
|
||||
A library for managing NTFS filesystems.
|
||||
|
||||
Attributes:
|
||||
logger (logging.Logger): Logger for the class.
|
||||
implementation (NTFSImplementation): The implementation to use for mounting NTFS filesystems.
|
||||
"""
|
||||
|
||||
def __init__(self, implementation):
|
||||
"""
|
||||
Initializes the instance with the given implementation.
|
||||
|
||||
Args:
|
||||
implementation: The implementation to be used by the instance.
|
||||
|
||||
Attributes:
|
||||
logger (logging.Logger): Logger instance for the class, set to debug level.
|
||||
implementation: The implementation provided during initialization.
|
||||
"""
|
||||
self.logger = logging.getLogger("NTFSLibrary")
|
||||
self.logger.setLevel(logging.DEBUG)
|
||||
self.implementation = implementation
|
||||
|
||||
self.logger.debug("Initializing")
|
||||
|
||||
def create_filesystem(self, device, label):
|
||||
"""
|
||||
Creates an NTFS filesystem on the specified device with the given label.
|
||||
|
||||
Args:
|
||||
device (str): The device path where the NTFS filesystem will be created.
|
||||
label (str): The label to assign to the NTFS filesystem.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Logs:
|
||||
Logs the creation process with the device and label information.
|
||||
"""
|
||||
self.logger.info(f"Creating NTFS in {device} with label {label}")
|
||||
|
||||
subprocess.run(["/usr/sbin/mkntfs", device, "-Q", "-L", label], check=True)
|
||||
|
||||
|
||||
def mount_filesystem(self, device, mountpoint):
|
||||
"""
|
||||
Mounts a filesystem on the specified mountpoint using the specified NTFS implementation.
|
||||
|
||||
Args:
|
||||
device (str): The device path to be mounted (e.g., '/dev/sda1').
|
||||
mountpoint (str): The directory where the device will be mounted.
|
||||
|
||||
Raises:
|
||||
ValueError: If the NTFS implementation is unknown.
|
||||
|
||||
"""
|
||||
self.logger.info(f"Mounting {device} in {mountpoint} using implementation {self.implementation}")
|
||||
if self.implementation == NTFSImplementation.KERNEL:
|
||||
subprocess.run(["/usr/bin/mount", "-t", "ntfs3", device, mountpoint], check = True)
|
||||
elif self.implementation == NTFSImplementation.NTFS3G:
|
||||
subprocess.run(["/usr/bin/ntfs-3g", device, mountpoint], check = True)
|
||||
else:
|
||||
raise ValueError("Unknown NTFS implementation: {self.implementation}")
|
||||
|
||||
def modify_uuid(self, device, uuid):
|
||||
"""
|
||||
Modify the UUID of an NTFS device.
|
||||
|
||||
This function changes the UUID of the specified NTFS device to the given UUID.
|
||||
It reads the current UUID from the device, logs the change, and writes the new UUID.
|
||||
|
||||
Args:
|
||||
device (str): The path to the NTFS device file.
|
||||
uuid (str): The new UUID to be set, in hexadecimal string format.
|
||||
|
||||
Raises:
|
||||
IOError: If there is an error opening or writing to the device file.
|
||||
"""
|
||||
|
||||
ntfs_uuid_offset = 0x48
|
||||
ntfs_uuid_length = 8
|
||||
|
||||
binary_uuid = bytearray.fromhex(uuid)
|
||||
binary_uuid.reverse()
|
||||
|
||||
self.logger.info(f"Changing UUID on {device} to {uuid}")
|
||||
with open(device, 'r+b') as ntfs_dev:
|
||||
self.logger.debug("Reading %i bytes from offset %i", ntfs_uuid_length, ntfs_uuid_offset)
|
||||
|
||||
ntfs_dev.seek(ntfs_uuid_offset)
|
||||
prev_uuid = bytearray(ntfs_dev.read(ntfs_uuid_length))
|
||||
prev_uuid.reverse()
|
||||
prev_uuid_hex = bytearray.hex(prev_uuid)
|
||||
self.logger.debug(f"Previous UUID: {prev_uuid_hex}")
|
||||
|
||||
self.logger.debug("Writing...")
|
||||
ntfs_dev.seek(ntfs_uuid_offset)
|
||||
ntfs_dev.write(binary_uuid)
|
|
@ -0,0 +1,11 @@
|
|||
gitdb==4.0.11
|
||||
GitPython==3.1.43
|
||||
libarchive-c==5.1
|
||||
nose==1.3.7
|
||||
pathlib==1.0.1
|
||||
pkg_resources==0.0.0
|
||||
pylibacl==0.7.0
|
||||
pylibblkid==0.3
|
||||
pyxattr==0.8.1
|
||||
smmap==5.0.1
|
||||
tqdm==4.66.5
|
|
@ -1,656 +0,0 @@
|
|||
opengnsys-gitinstaller (0.5dev3) UNRELEASED; urgency=medium
|
||||
|
||||
[ OpenGnsys ]
|
||||
* Initial release.
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* First commit
|
||||
* Add installer
|
||||
* Add requirements file
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Creates first skeleton of symfony+swagger project
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Add Gitlib
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Changes OgBootBundle name and adds a first endpoint to test
|
||||
* refs #734 Adds template of repository and branch endpoints
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Update docs to account for changes
|
||||
* Trivial API server
|
||||
* Ticket #753: Add repository listing
|
||||
* Ticket #735: List branches in repo
|
||||
* Add testing instructions
|
||||
* Agregar manejo de errrores
|
||||
* Ticket #741: Crear repo Ticket #736: Eliminar repo
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Adds README for Api installation
|
||||
* refs #734 Control of errores and http codes in controler
|
||||
* refs #734 Renemas oggitservice
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #738, ticket #739: repo and sync backup protoype
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Adds new endpoints sync and backup and status endpoint
|
||||
* refs #734 Adds nelmio api doc configuration
|
||||
* Adds .env file to root
|
||||
* refs #734 use environment variables in .env files and disable web depuration toolbar
|
||||
* refs #734 fix typo in .env and use oggit_url environment variable
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #738, ticket #739: git sync and backup
|
||||
|
||||
[ Nicolas Arenas ]
|
||||
* Add docker container files
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #737: GC
|
||||
* Use Paramiko and Gitpython for backups
|
||||
|
||||
[ Nicolas Arenas ]
|
||||
* Add mock api for testing dockerfile
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #740, listen on all hosts
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Removes innecesaries parameters and changes php platform to 8.2
|
||||
* refs #734 just changes name and description in swagger web page
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Remove duplicated import
|
||||
* Documentation prototype
|
||||
* Update to 24.04, solves deployment issue
|
||||
* Add more documentation
|
||||
* Add API README
|
||||
* Add API examples
|
||||
* Update list of package requirements in oglive
|
||||
* Fix commandline parsing bug
|
||||
* Revert experimental Windows change
|
||||
* Fix ticket #770: Re-parse filesystems list after mounting
|
||||
* Use oglive server if ogrepository is not set
|
||||
* Ticket #770: Add sanity check
|
||||
* Ticket #771: Correctly create directories on metadata restoration
|
||||
* Ticket #780: Unmount before clone if needed
|
||||
* Fix ticket #800: sudo doesn't work
|
||||
|
||||
[ Vadim Trochinsky ]
|
||||
* Fix ticket #802: .git directory in filesystem root
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Fix ticket #805: Remove .git directory if it already exists when checking out
|
||||
* Ticket #770: Correctly update metadata when mounting and unmounting
|
||||
* Ticket #804: Move log
|
||||
* Fix ticket #902: .git directories can't be checked out
|
||||
* Lint fixes
|
||||
* Remove unused code
|
||||
* Lint fixes
|
||||
* Lint fixes
|
||||
* Lint fixes
|
||||
* Additional logging message
|
||||
* Lint fix
|
||||
* Fix ticket #907: mknod fails due to path not found
|
||||
* Initial implementation for commit, push, fetch.
|
||||
* Don't fail on empty lines in metadata, just skip them
|
||||
* Add documentation and functionality to progress hook (not used yet)
|
||||
* Pylint fixes
|
||||
* Ticket #908: Remove some unneeded warnings
|
||||
* Fix progress report
|
||||
* Ticket #906: Fix permissions on directories
|
||||
* Make pylint happy
|
||||
* Mount fix
|
||||
* Ticket #808: Initial implementation
|
||||
* Initial forgejo install
|
||||
* Deduplicate key extraction
|
||||
* Fix installer bugs and add documentation
|
||||
* Change user to oggit
|
||||
* Fix NTFS ID modification implementation
|
||||
* Implement system-specific EFI data support
|
||||
* Fix encoding when reading system uuid
|
||||
* Fix and refactor slightly EFI implementation
|
||||
* Add Windows BCD decoding tool
|
||||
* Check module loading and unloading, modprobe works on oglive now
|
||||
* Make EFI deployment more flexible
|
||||
* Add organization API call
|
||||
* Fix bash library path
|
||||
* Fix repo paths for forgejo
|
||||
* Update documentation
|
||||
* Sync to ensure everything is written
|
||||
* Refactoring and more pydoc
|
||||
* Add more documentation
|
||||
* Improve installer documentation
|
||||
* Improve gitlib instructions
|
||||
* Add missing files
|
||||
* Partial setsshkey implementation
|
||||
* Fix SSH key generation and extraction
|
||||
* Initial package contents
|
||||
* Add Debian packaging
|
||||
* Add pylkid
|
||||
* Add pyblkid debian files
|
||||
* Use packaged pyblkid
|
||||
* More detailed API logging
|
||||
* Improve logging
|
||||
* Add oglive key to forgejo
|
||||
* Add original source
|
||||
* Always re-download forgejo, even if installed.
|
||||
* Remove obsolete code that stopped being relevant with Forgejo
|
||||
* Move python modules to /opt/opengnsys-modules
|
||||
* Use absolute paths in initrd modification
|
||||
* Add timestamp to ssh key title, forgejo doesn't like duplicates
|
||||
* Skip past symlinks and problems in oglive modification
|
||||
* Get keys from squashfs instead of initrd to work with current oglive packaging
|
||||
* Fix trivial bug
|
||||
* Move modules to /usr/share/opengnsys
|
||||
* Move packages to /usr/share
|
||||
|
||||
[ Angel Rodriguez ]
|
||||
* Add gitlib/README-en.md
|
||||
* Add api/README-en.md
|
||||
* Add installer/README-en.md
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Skip NTFS code on non-Windows
|
||||
* Store and restore GPT partition UUIDs
|
||||
* Update READMEs
|
||||
* BCD constants
|
||||
* Use tqdm
|
||||
* Constants
|
||||
* Add extra mounts update
|
||||
* Better status reports
|
||||
* Make log filename machine-dependent Move kernel args parsing
|
||||
* Make unmounting more robust
|
||||
* Improve repository initialization
|
||||
* Make --pull work like the other commands
|
||||
* Add packages
|
||||
* Update documentation
|
||||
* Ignore python cache
|
||||
* Ignore more files
|
||||
* add python libarchive-c original package
|
||||
* Add pyblkid copyright file
|
||||
* Add make_orig script
|
||||
* Reorder and fix for ogrepository reorganization
|
||||
* Restructure git installer to work without ogboot on the same machine, update docs
|
||||
* Update english documentation
|
||||
* Improve installation process, make it possible to extract keys from oglive
|
||||
* Fix namespaces
|
||||
* Fix ogrepository paths
|
||||
* Change git repo path
|
||||
* Improvements for logging and error handling
|
||||
* Fix HTTP exception handling
|
||||
* Improve task management, cleanup when there are too many
|
||||
* More error logging
|
||||
* Mark git repo as a safe directory
|
||||
* Rework the ability to use a custom SSH key
|
||||
* Log every request
|
||||
* Branch deletion
|
||||
* Make branch deletion RESTful
|
||||
* Initial version of the API server
|
||||
* Add original repo_api
|
||||
* Convert to blueprint
|
||||
* Add port argument
|
||||
* Fix error handling
|
||||
* Add README
|
||||
* Load swagger from disk
|
||||
* Fix repository URL
|
||||
* Bump forgejo version
|
||||
* Add helpful script
|
||||
* Fix port argument
|
||||
* Refactoring for package support
|
||||
* Remove old code
|
||||
* Refactoring for packaging
|
||||
* opengnsys-forgejo package
|
||||
* Fix post-install for forgejo deployment
|
||||
* Fixes for running under gunicorn
|
||||
* Debian packaging
|
||||
* Add branches and tags creation endpoints
|
||||
* Add missing file
|
||||
* Rename service
|
||||
* Add templates
|
||||
* Disable tests
|
||||
* Fix permission problem
|
||||
* Fix ini path
|
||||
* Update changelog
|
||||
* Update changelog
|
||||
* Add package files
|
||||
* Add git image creation script
|
||||
* Slightly improve API for ogrepo usability
|
||||
* First commit
|
||||
* Add installer
|
||||
* Add requirements file
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Creates first skeleton of symfony+swagger project
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Add Gitlib
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Changes OgBootBundle name and adds a first endpoint to test
|
||||
* refs #734 Adds template of repository and branch endpoints
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Update docs to account for changes
|
||||
* Trivial API server
|
||||
* Ticket #753: Add repository listing
|
||||
* Ticket #735: List branches in repo
|
||||
* Add testing instructions
|
||||
* Agregar manejo de errrores
|
||||
* Ticket #741: Crear repo Ticket #736: Eliminar repo
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Adds README for Api installation
|
||||
* refs #734 Control of errores and http codes in controler
|
||||
* refs #734 Renemas oggitservice
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #738, ticket #739: repo and sync backup protoype
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Adds new endpoints sync and backup and status endpoint
|
||||
* refs #734 Adds nelmio api doc configuration
|
||||
* Adds .env file to root
|
||||
* refs #734 use environment variables in .env files and disable web depuration toolbar
|
||||
* refs #734 fix typo in .env and use oggit_url environment variable
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #738, ticket #739: git sync and backup
|
||||
|
||||
[ Nicolas Arenas ]
|
||||
* Add docker container files
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #737: GC
|
||||
* Use Paramiko and Gitpython for backups
|
||||
|
||||
[ Nicolas Arenas ]
|
||||
* Add mock api for testing dockerfile
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #740, listen on all hosts
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Removes innecesaries parameters and changes php platform to 8.2
|
||||
* refs #734 just changes name and description in swagger web page
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Remove duplicated import
|
||||
* Documentation prototype
|
||||
* Update to 24.04, solves deployment issue
|
||||
* Add more documentation
|
||||
* Add API README
|
||||
* Add API examples
|
||||
* Update list of package requirements in oglive
|
||||
* Fix commandline parsing bug
|
||||
* Revert experimental Windows change
|
||||
* Fix ticket #770: Re-parse filesystems list after mounting
|
||||
* Use oglive server if ogrepository is not set
|
||||
* Ticket #770: Add sanity check
|
||||
* Ticket #771: Correctly create directories on metadata restoration
|
||||
* Ticket #780: Unmount before clone if needed
|
||||
* Fix ticket #800: sudo doesn't work
|
||||
|
||||
[ Vadim Trochinsky ]
|
||||
* Fix ticket #802: .git directory in filesystem root
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Fix ticket #805: Remove .git directory if it already exists when checking out
|
||||
* Ticket #770: Correctly update metadata when mounting and unmounting
|
||||
* Ticket #804: Move log
|
||||
* Fix ticket #902: .git directories can't be checked out
|
||||
* Lint fixes
|
||||
* Remove unused code
|
||||
* Lint fixes
|
||||
* Lint fixes
|
||||
* Lint fixes
|
||||
* Additional logging message
|
||||
* Lint fix
|
||||
* Fix ticket #907: mknod fails due to path not found
|
||||
* Initial implementation for commit, push, fetch.
|
||||
* Don't fail on empty lines in metadata, just skip them
|
||||
* Add documentation and functionality to progress hook (not used yet)
|
||||
* Pylint fixes
|
||||
* Ticket #908: Remove some unneeded warnings
|
||||
* Fix progress report
|
||||
* Ticket #906: Fix permissions on directories
|
||||
* Make pylint happy
|
||||
* Mount fix
|
||||
* Ticket #808: Initial implementation
|
||||
* Initial forgejo install
|
||||
* Deduplicate key extraction
|
||||
* Fix installer bugs and add documentation
|
||||
* Change user to oggit
|
||||
* Fix NTFS ID modification implementation
|
||||
* Implement system-specific EFI data support
|
||||
* Fix encoding when reading system uuid
|
||||
* Fix and refactor slightly EFI implementation
|
||||
* Add Windows BCD decoding tool
|
||||
* Check module loading and unloading, modprobe works on oglive now
|
||||
* Make EFI deployment more flexible
|
||||
* Add organization API call
|
||||
* Fix bash library path
|
||||
* Fix repo paths for forgejo
|
||||
* Update documentation
|
||||
* Sync to ensure everything is written
|
||||
* Refactoring and more pydoc
|
||||
* Add more documentation
|
||||
* Improve installer documentation
|
||||
* Improve gitlib instructions
|
||||
* Add missing files
|
||||
* Partial setsshkey implementation
|
||||
* Fix SSH key generation and extraction
|
||||
* Initial package contents
|
||||
* Add Debian packaging
|
||||
* Add pylkid
|
||||
* Add pyblkid debian files
|
||||
* Use packaged pyblkid
|
||||
* More detailed API logging
|
||||
* Improve logging
|
||||
* Add oglive key to forgejo
|
||||
* Add original source
|
||||
* Always re-download forgejo, even if installed.
|
||||
* Remove obsolete code that stopped being relevant with Forgejo
|
||||
* Move python modules to /opt/opengnsys-modules
|
||||
* Use absolute paths in initrd modification
|
||||
* Add timestamp to ssh key title, forgejo doesn't like duplicates
|
||||
* Skip past symlinks and problems in oglive modification
|
||||
* Get keys from squashfs instead of initrd to work with current oglive packaging
|
||||
* Fix trivial bug
|
||||
* Move modules to /usr/share/opengnsys
|
||||
* Move packages to /usr/share
|
||||
|
||||
[ Angel Rodriguez ]
|
||||
* Add gitlib/README-en.md
|
||||
* Add api/README-en.md
|
||||
* Add installer/README-en.md
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Skip NTFS code on non-Windows
|
||||
* Store and restore GPT partition UUIDs
|
||||
* Update READMEs
|
||||
* BCD constants
|
||||
* Use tqdm
|
||||
* Constants
|
||||
* Add extra mounts update
|
||||
* Better status reports
|
||||
* Make log filename machine-dependent Move kernel args parsing
|
||||
* Make unmounting more robust
|
||||
* Improve repository initialization
|
||||
* Make --pull work like the other commands
|
||||
* Add packages
|
||||
* Update documentation
|
||||
* Ignore python cache
|
||||
* Ignore more files
|
||||
* add python libarchive-c original package
|
||||
* Add pyblkid copyright file
|
||||
* Add make_orig script
|
||||
* Reorder and fix for ogrepository reorganization
|
||||
* Restructure git installer to work without ogboot on the same machine, update docs
|
||||
* Update english documentation
|
||||
* Improve installation process, make it possible to extract keys from oglive
|
||||
* Fix namespaces
|
||||
* Fix ogrepository paths
|
||||
* Change git repo path
|
||||
* Improvements for logging and error handling
|
||||
* Fix HTTP exception handling
|
||||
* Improve task management, cleanup when there are too many
|
||||
* More error logging
|
||||
* Mark git repo as a safe directory
|
||||
* Rework the ability to use a custom SSH key
|
||||
* Log every request
|
||||
* Branch deletion
|
||||
* Make branch deletion RESTful
|
||||
* Initial version of the API server
|
||||
* Add original repo_api
|
||||
* Convert to blueprint
|
||||
* Add port argument
|
||||
* Fix error handling
|
||||
* Add README
|
||||
* Load swagger from disk
|
||||
* Fix repository URL
|
||||
* Bump forgejo version
|
||||
* Add helpful script
|
||||
* Fix port argument
|
||||
* Refactoring for package support
|
||||
* Remove old code
|
||||
* Refactoring for packaging
|
||||
* opengnsys-forgejo package
|
||||
* Fix post-install for forgejo deployment
|
||||
* Fixes for running under gunicorn
|
||||
* Debian packaging
|
||||
* Add branches and tags creation endpoints
|
||||
* Add missing file
|
||||
* Rename service
|
||||
* Add templates
|
||||
* Disable tests
|
||||
* Fix permission problem
|
||||
* Fix ini path
|
||||
* Update changelog
|
||||
* Update changelog
|
||||
* Add package files
|
||||
* Add git image creation script
|
||||
* Slightly improve API for ogrepo usability
|
||||
* Update changelog
|
||||
* First commit
|
||||
* Add installer
|
||||
* Add requirements file
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Creates first skeleton of symfony+swagger project
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Add Gitlib
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Changes OgBootBundle name and adds a first endpoint to test
|
||||
* refs #734 Adds template of repository and branch endpoints
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Update docs to account for changes
|
||||
* Trivial API server
|
||||
* Ticket #753: Add repository listing
|
||||
* Ticket #735: List branches in repo
|
||||
* Add testing instructions
|
||||
* Agregar manejo de errrores
|
||||
* Ticket #741: Crear repo Ticket #736: Eliminar repo
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Adds README for Api installation
|
||||
* refs #734 Control of errores and http codes in controler
|
||||
* refs #734 Renemas oggitservice
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #738, ticket #739: repo and sync backup protoype
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Adds new endpoints sync and backup and status endpoint
|
||||
* refs #734 Adds nelmio api doc configuration
|
||||
* Adds .env file to root
|
||||
* refs #734 use environment variables in .env files and disable web depuration toolbar
|
||||
* refs #734 fix typo in .env and use oggit_url environment variable
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #738, ticket #739: git sync and backup
|
||||
|
||||
[ Nicolas Arenas ]
|
||||
* Add docker container files
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #737: GC
|
||||
* Use Paramiko and Gitpython for backups
|
||||
|
||||
[ Nicolas Arenas ]
|
||||
* Add mock api for testing dockerfile
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Ticket #740, listen on all hosts
|
||||
|
||||
[ lgromero ]
|
||||
* refs #734 Removes innecesaries parameters and changes php platform to 8.2
|
||||
* refs #734 just changes name and description in swagger web page
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Remove duplicated import
|
||||
* Documentation prototype
|
||||
* Update to 24.04, solves deployment issue
|
||||
* Add more documentation
|
||||
* Add API README
|
||||
* Add API examples
|
||||
* Update list of package requirements in oglive
|
||||
* Fix commandline parsing bug
|
||||
* Revert experimental Windows change
|
||||
* Fix ticket #770: Re-parse filesystems list after mounting
|
||||
* Use oglive server if ogrepository is not set
|
||||
* Ticket #770: Add sanity check
|
||||
* Ticket #771: Correctly create directories on metadata restoration
|
||||
* Ticket #780: Unmount before clone if needed
|
||||
* Fix ticket #800: sudo doesn't work
|
||||
|
||||
[ Vadim Trochinsky ]
|
||||
* Fix ticket #802: .git directory in filesystem root
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Fix ticket #805: Remove .git directory if it already exists when checking out
|
||||
* Ticket #770: Correctly update metadata when mounting and unmounting
|
||||
* Ticket #804: Move log
|
||||
* Fix ticket #902: .git directories can't be checked out
|
||||
* Lint fixes
|
||||
* Remove unused code
|
||||
* Lint fixes
|
||||
* Lint fixes
|
||||
* Lint fixes
|
||||
* Additional logging message
|
||||
* Lint fix
|
||||
* Fix ticket #907: mknod fails due to path not found
|
||||
* Initial implementation for commit, push, fetch.
|
||||
* Don't fail on empty lines in metadata, just skip them
|
||||
* Add documentation and functionality to progress hook (not used yet)
|
||||
* Pylint fixes
|
||||
* Ticket #908: Remove some unneeded warnings
|
||||
* Fix progress report
|
||||
* Ticket #906: Fix permissions on directories
|
||||
* Make pylint happy
|
||||
* Mount fix
|
||||
* Ticket #808: Initial implementation
|
||||
* Initial forgejo install
|
||||
* Deduplicate key extraction
|
||||
* Fix installer bugs and add documentation
|
||||
* Change user to oggit
|
||||
* Fix NTFS ID modification implementation
|
||||
* Implement system-specific EFI data support
|
||||
* Fix encoding when reading system uuid
|
||||
* Fix and refactor slightly EFI implementation
|
||||
* Add Windows BCD decoding tool
|
||||
* Check module loading and unloading, modprobe works on oglive now
|
||||
* Make EFI deployment more flexible
|
||||
* Add organization API call
|
||||
* Fix bash library path
|
||||
* Fix repo paths for forgejo
|
||||
* Update documentation
|
||||
* Sync to ensure everything is written
|
||||
* Refactoring and more pydoc
|
||||
* Add more documentation
|
||||
* Improve installer documentation
|
||||
* Improve gitlib instructions
|
||||
* Add missing files
|
||||
* Partial setsshkey implementation
|
||||
* Fix SSH key generation and extraction
|
||||
* Initial package contents
|
||||
* Add Debian packaging
|
||||
* Add pylkid
|
||||
* Add pyblkid debian files
|
||||
* Use packaged pyblkid
|
||||
* More detailed API logging
|
||||
* Improve logging
|
||||
* Add oglive key to forgejo
|
||||
* Add original source
|
||||
* Always re-download forgejo, even if installed.
|
||||
* Remove obsolete code that stopped being relevant with Forgejo
|
||||
* Move python modules to /opt/opengnsys-modules
|
||||
* Use absolute paths in initrd modification
|
||||
* Add timestamp to ssh key title, forgejo doesn't like duplicates
|
||||
* Skip past symlinks and problems in oglive modification
|
||||
* Get keys from squashfs instead of initrd to work with current oglive packaging
|
||||
* Fix trivial bug
|
||||
* Move modules to /usr/share/opengnsys
|
||||
* Move packages to /usr/share
|
||||
|
||||
[ Angel Rodriguez ]
|
||||
* Add gitlib/README-en.md
|
||||
* Add api/README-en.md
|
||||
* Add installer/README-en.md
|
||||
|
||||
[ Vadim Troshchinskiy ]
|
||||
* Skip NTFS code on non-Windows
|
||||
* Store and restore GPT partition UUIDs
|
||||
* Update READMEs
|
||||
* BCD constants
|
||||
* Use tqdm
|
||||
* Constants
|
||||
* Add extra mounts update
|
||||
* Better status reports
|
||||
* Make log filename machine-dependent Move kernel args parsing
|
||||
* Make unmounting more robust
|
||||
* Improve repository initialization
|
||||
* Make --pull work like the other commands
|
||||
* Add packages
|
||||
* Update documentation
|
||||
* Ignore python cache
|
||||
* Ignore more files
|
||||
* add python libarchive-c original package
|
||||
* Add pyblkid copyright file
|
||||
* Add make_orig script
|
||||
* Reorder and fix for ogrepository reorganization
|
||||
* Restructure git installer to work without ogboot on the same machine, update docs
|
||||
* Update english documentation
|
||||
* Improve installation process, make it possible to extract keys from oglive
|
||||
* Fix namespaces
|
||||
* Fix ogrepository paths
|
||||
* Change git repo path
|
||||
* Improvements for logging and error handling
|
||||
* Fix HTTP exception handling
|
||||
* Improve task management, cleanup when there are too many
|
||||
* More error logging
|
||||
* Mark git repo as a safe directory
|
||||
* Rework the ability to use a custom SSH key
|
||||
* Log every request
|
||||
* Branch deletion
|
||||
* Make branch deletion RESTful
|
||||
* Initial version of the API server
|
||||
* Add original repo_api
|
||||
* Convert to blueprint
|
||||
* Add port argument
|
||||
* Fix error handling
|
||||
* Add README
|
||||
* Load swagger from disk
|
||||
* Fix repository URL
|
||||
* Bump forgejo version
|
||||
* Add helpful script
|
||||
* Fix port argument
|
||||
* Refactoring for package support
|
||||
* Remove old code
|
||||
* Refactoring for packaging
|
||||
* opengnsys-forgejo package
|
||||
* Fix post-install for forgejo deployment
|
||||
* Fixes for running under gunicorn
|
||||
* Debian packaging
|
||||
* Add branches and tags creation endpoints
|
||||
* Add missing file
|
||||
* Rename service
|
||||
* Add templates
|
||||
* Disable tests
|
||||
* Fix permission problem
|
||||
* Fix ini path
|
||||
* Update changelog
|
||||
* Update changelog
|
||||
* Add package files
|
||||
* Add git image creation script
|
||||
* Slightly improve API for ogrepo usability
|
||||
* Update changelog
|
||||
* Update changelog
|
||||
|
||||
-- OpenGnsys <opengnsys@opengnsys.com> Mon, 16 Jun 2025 21:23:34 +0000
|
|
@ -1,2 +0,0 @@
|
|||
opengnsys-gitinstaller_0.5_amd64.buildinfo unknown optional
|
||||
opengnsys-gitinstaller_0.5_amd64.deb unknown optional
|
|
@ -1 +0,0 @@
|
|||
/opt/opengnsys/ogrepository/oggit/lib
|
|
@ -1 +0,0 @@
|
|||
opengnsys_git_installer.py /opt/opengnsys/ogrepository/oggit/lib
|
|
@ -219,7 +219,7 @@ class OpengnsysGitInstaller:
|
|||
self.forgejo_user = "oggit"
|
||||
self.forgejo_password = "opengnsys"
|
||||
self.forgejo_organization = "opengnsys"
|
||||
self.forgejo_port = 3100
|
||||
self.forgejo_port = 3000
|
||||
|
||||
self.forgejo_bin_path = os.path.join(self.ogrepository_base_path, "bin")
|
||||
self.forgejo_exe = os.path.join(self.forgejo_bin_path, "forgejo")
|
||||
|
@ -431,13 +431,11 @@ class OpengnsysGitInstaller:
|
|||
name = os.path.basename(squashfs_file)
|
||||
|
||||
|
||||
keys = self.extract_ssh_keys_from_squashfs(oglive_num = oglive_num, squashfs_file=squashfs_file)
|
||||
retvals = []
|
||||
keys = installer.extract_ssh_keys_from_squashfs(oglive_num = oglive_num, squashfs_file=squashfs_file)
|
||||
for k in keys:
|
||||
timestamp = '{:%Y-%m-%d %H:%M:%S}'.format(datetime.datetime.now())
|
||||
retvals = retvals + [self.add_forgejo_sshkey(k, f"Key for {name} ({timestamp})")]
|
||||
installer.add_forgejo_sshkey(k, f"Key for {name} ({timestamp})")
|
||||
|
||||
return retvals
|
||||
|
||||
|
||||
def extract_ssh_keys_from_squashfs(self, oglive_num = None, squashfs_file = None):
|
||||
|
@ -1016,7 +1014,6 @@ class OpengnsysGitInstaller:
|
|||
)
|
||||
|
||||
self.__logger.info("Request status was %i, content %s", r.status_code, r.content)
|
||||
return r.status_code, r.content.decode('utf-8')
|
||||
|
||||
def add_forgejo_organization(self, pubkey, description = ""):
|
||||
token = ""
|
||||
|
@ -1071,15 +1068,6 @@ if __name__ == '__main__':
|
|||
logger.info("Running as post-install script")
|
||||
installer=OpengnsysGitInstaller()
|
||||
|
||||
logger.debug("Obtaining configuration from debconf")
|
||||
import debconf
|
||||
with debconf.Debconf(run_frontend=True) as db:
|
||||
installer.forgejo_organization = db.get('opengnsys/forgejo_organization')
|
||||
installer.forgejo_user = db.get('opengnsys/forgejo_user')
|
||||
installer.forgejo_password = db.get('opengnsys/forgejo_password')
|
||||
installer.email = db.get('opengnsys/forgejo_email')
|
||||
installer.forgejo_port = int(db.get('opengnsys/forgejo_port'))
|
||||
|
||||
# Templates get installed here
|
||||
installer.template_path = "/usr/share/opengnsys-forgejo/"
|
||||
installer.configure_forgejo()
|
||||
|
|
|
@ -18,8 +18,5 @@ override_dh_gencontrol:
|
|||
override_dh_installdocs:
|
||||
# Nothing, we don't want docs
|
||||
|
||||
override_dh_auto_test:
|
||||
# Nothing
|
||||
#
|
||||
override_dh_installchangelogs:
|
||||
# Nothing, we don't want the changelog
|
||||
|
|
|
@ -1,29 +1,5 @@
|
|||
opengnsys-forgejo (0.5.5) UNRELEASED; urgency=medium
|
||||
opengnsys-forgejo (0.5) UNRELEASED; urgency=medium
|
||||
|
||||
* refs #2458 Agregar clave de SSH
|
||||
* Initial release.
|
||||
|
||||
-- OpenGnsys <opengnsys@opengnsys.com> Tue, 14 Jul 2025 11:10:20 +0000
|
||||
|
||||
opengnsys-forgejo (0.5.4) UNRELEASED; urgency=medium
|
||||
|
||||
* refs #2364 Fix incorrect template defaults
|
||||
|
||||
-- OpenGnsys <opengnsys@opengnsys.com> Tue, 01 Jul 2025 16:20:30 +0000
|
||||
|
||||
opengnsys-forgejo (0.5.3) UNRELEASED; urgency=medium
|
||||
|
||||
* refs #2364 Update forgejo to LTS version
|
||||
|
||||
-- OpenGnsys <opengnsys@opengnsys.com> Tue, 01 Jul 2025 12:15:35 +0000
|
||||
|
||||
opengnsys-forgejo (0.5.2) UNRELEASED; urgency=medium
|
||||
|
||||
* refs #2364 Redo post-install script, minimize dependencies
|
||||
|
||||
-- OpenGnsys <opengnsys@opengnsys.com> Tue, 01 Jul 2025 12:00:30 +0000
|
||||
|
||||
opengnsys-forgejo (0.5.1dev1) UNRELEASED; urgency=medium
|
||||
|
||||
* Initial version
|
||||
|
||||
-- OpenGnsys <opengnsys@opengnsys.com> Thu, 05 Jun 2025 21:46:30 +0000
|
||||
-- OpenGnsys <opengnsys@opengnsys.es> Fri, 14 Mar 2025 08:40:35 +0100
|
||||
|
|
|
@ -18,9 +18,20 @@ Depends:
|
|||
${misc:Depends},
|
||||
bsdextrautils,
|
||||
debconf (>= 1.5.0),
|
||||
python3 (>= 3.12.0),
|
||||
python3-requests (>= 2.31),
|
||||
python3-debconf (>= 1.5.0)
|
||||
gunicorn,
|
||||
opengnsys-flask-executor,
|
||||
opengnsys-flask-restx,
|
||||
opengnsys-libarchive-c,
|
||||
python3,
|
||||
python3-aniso8601,
|
||||
python3-flasgger,
|
||||
python3-flask,
|
||||
python3-flask,
|
||||
python3-git,
|
||||
python3-paramiko,
|
||||
python3-requests,
|
||||
python3-termcolor,
|
||||
python3-tqdm
|
||||
Conflicts:
|
||||
Description: Opengnsys Forgejo package for OgGit
|
||||
Forgejo installation configured for OpenGnsys
|
||||
Files for OpenGnsys Git support
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
forgejo /opt/opengnsys/ogrepository/bin
|
||||
forgejo-app.ini /usr/share/opengnsys-forgejo/
|
||||
opengnsys-forgejo.service /usr/share/opengnsys-forgejo/
|
||||
opengnsys.pub /usr/share/opengnsys-forgejo/
|
||||
opengnsys-forgejo.service /usr/share/opengnsys-forgejo/
|
|
@ -1,496 +0,0 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Script para la instalación del repositorio git"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
import subprocess
|
||||
import pwd
|
||||
import grp
|
||||
|
||||
import pathlib
|
||||
import socket
|
||||
import time
|
||||
import requests
|
||||
import debconf
|
||||
|
||||
class OpengnsysForgejoPostInstall:
|
||||
"""Opengnsys Forgejo Post-installer"""
|
||||
|
||||
def __init__(self):
|
||||
"""Inicializar clase"""
|
||||
self.__logger = logging.getLogger("OpengnsysForgejoInstaller")
|
||||
self.__logger.setLevel(logging.DEBUG)
|
||||
self.__logger.debug("Initializing")
|
||||
self.testmode = False
|
||||
self.base_path = "/opt/opengnsys"
|
||||
self.ogrepository_base_path = os.path.join(self.base_path, "ogrepository")
|
||||
self.git_basedir = "base.git"
|
||||
self.email = "OpenGnsys@opengnsys.com"
|
||||
|
||||
self.opengnsys_bin_path = os.path.join(self.base_path, "bin")
|
||||
self.opengnsys_etc_path = os.path.join(self.base_path, "etc")
|
||||
|
||||
self.forgejo_user = "oggit"
|
||||
self.forgejo_password = "opengnsys"
|
||||
self.forgejo_organization = "opengnsys"
|
||||
self.forgejo_port = 3100
|
||||
|
||||
self.forgejo_bin_path = os.path.join(self.ogrepository_base_path, "bin")
|
||||
self.forgejo_exe = os.path.join(self.forgejo_bin_path, "forgejo")
|
||||
self.forgejo_conf_dir_path = os.path.join(self.ogrepository_base_path, "etc", "forgejo")
|
||||
|
||||
self.lfs_dir_path = os.path.join(self.ogrepository_base_path, "oggit", "git-lfs")
|
||||
self.git_dir_path = os.path.join(self.ogrepository_base_path, "oggit", "git")
|
||||
|
||||
self.forgejo_var_dir_path = os.path.join(self.ogrepository_base_path, "var", "lib", "forgejo")
|
||||
self.forgejo_work_dir_path = os.path.join(self.forgejo_var_dir_path, "work")
|
||||
self.forgejo_work_custom_dir_path = os.path.join(self.forgejo_work_dir_path, "custom")
|
||||
self.forgejo_db_dir_path = os.path.join(self.forgejo_var_dir_path, "db")
|
||||
self.forgejo_data_dir_path = os.path.join(self.forgejo_var_dir_path, "data")
|
||||
|
||||
self.forgejo_db_path = os.path.join(self.forgejo_db_dir_path, "forgejo.db")
|
||||
|
||||
self.forgejo_log_dir_path = os.path.join(self.ogrepository_base_path, "log", "forgejo")
|
||||
self.forgejo_ini_file_path = os.path.join(self.forgejo_conf_dir_path, "app.ini")
|
||||
|
||||
self.ssh_group = "oggit"
|
||||
self.ssh_user = "oggit"
|
||||
self.ssh_uid = -1
|
||||
self.ssh_gid = -1
|
||||
self.ssh_homedir = None
|
||||
|
||||
self.temp_dir = None
|
||||
self.script_path = os.path.realpath(os.path.dirname(__file__))
|
||||
|
||||
# Where we look for forgejo-app.ini and similar templates.
|
||||
self.template_path = self.script_path
|
||||
|
||||
def _apply_configuration(self):
|
||||
"""Do anything that requires recalculating variables based on other variables.
|
||||
|
||||
This ensures any changes made based on debconf configs are acted on.
|
||||
"""
|
||||
self.set_ssh_user_group(self.ssh_user, self.ssh_group)
|
||||
|
||||
|
||||
def set_ssh_user_group(self, username, groupname):
|
||||
"""
|
||||
Ensures that the specified SSH user and group exist on the system, creating them if necessary.
|
||||
|
||||
Args:
|
||||
username (str): The name of the SSH user to ensure exists.
|
||||
groupname (str): The name of the SSH group to ensure exists.
|
||||
|
||||
Side Effects:
|
||||
- Sets instance attributes: ssh_group, ssh_user, ssh_gid, ssh_uid, ssh_homedir.
|
||||
- Logs actions and results using the instance logger.
|
||||
- May create a new system group and/or user if they do not already exist.
|
||||
|
||||
Raises:
|
||||
subprocess.CalledProcessError: If group or user creation commands fail.
|
||||
"""
|
||||
|
||||
|
||||
self.ssh_group = groupname
|
||||
self.ssh_user = username
|
||||
|
||||
try:
|
||||
self.ssh_gid = grp.getgrnam(self.ssh_group).gr_gid
|
||||
self.__logger.info("Group %s exists with gid %i", self.ssh_group, self.ssh_gid)
|
||||
except KeyError:
|
||||
self.__logger.info("Need to create group %s", self.ssh_group)
|
||||
subprocess.run(["/usr/sbin/groupadd", "--system", self.ssh_group], check=True)
|
||||
self.ssh_gid = grp.getgrnam(groupname).gr_gid
|
||||
|
||||
|
||||
try:
|
||||
self.ssh_uid = pwd.getpwnam(self.ssh_user).pw_uid
|
||||
self.__logger.info("User %s exists with gid %i", self.ssh_user, self.ssh_uid)
|
||||
except KeyError:
|
||||
self.__logger.info("Need to create user %s", self.ssh_user)
|
||||
subprocess.run(["/usr/sbin/useradd", "--gid", str(self.ssh_gid), "-m", "--system", self.ssh_user], check=True)
|
||||
self.ssh_uid = pwd.getpwnam(username).pw_uid
|
||||
|
||||
self.ssh_homedir = pwd.getpwnam(username).pw_dir
|
||||
|
||||
|
||||
def _recursive_chown(self, path, ouid, ogid):
|
||||
"""
|
||||
Recursively change the owner and group of a directory and all its files.
|
||||
|
||||
Args:
|
||||
path (str): The root directory path to start changing ownership.
|
||||
ouid (int): The user ID to set as the owner.
|
||||
ogid (int): The group ID to set as the group.
|
||||
|
||||
Raises:
|
||||
OSError: If changing ownership fails for any file or directory.
|
||||
"""
|
||||
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 _wait_for_port(self, host, port):
|
||||
self.__logger.info("Waiting for %s:%i to be up", host, port)
|
||||
|
||||
timeout = 60
|
||||
start_time = time.time()
|
||||
|
||||
ready = False
|
||||
while not ready and (time.time() - start_time) < 60:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
try:
|
||||
s.connect((host, port))
|
||||
ready = True
|
||||
s.close()
|
||||
except TimeoutError:
|
||||
self.__logger.debug("Timed out, no connection yet.")
|
||||
except OSError as oserr:
|
||||
self.__logger.debug("%s, no connection yet. %.1f seconds left.", oserr.strerror, timeout - (time.time() - start_time))
|
||||
|
||||
time.sleep(0.1)
|
||||
|
||||
if ready:
|
||||
self.__logger.info("Connection established.")
|
||||
else:
|
||||
self.__logger.error("Timed out waiting for connection!")
|
||||
raise TimeoutError("Timed out waiting for connection!")
|
||||
|
||||
def _install_template(self, template, destination, keysvalues):
|
||||
|
||||
self.__logger.info("Writing template %s into %s", template, destination)
|
||||
|
||||
data = ""
|
||||
with open(template, "r", encoding="utf-8") as template_file:
|
||||
data = template_file.read()
|
||||
|
||||
for key in keysvalues.keys():
|
||||
if isinstance(keysvalues[key], int):
|
||||
data = data.replace("{" + key + "}", str(keysvalues[key]))
|
||||
else:
|
||||
data = data.replace("{" + key + "}", keysvalues[key])
|
||||
|
||||
with open(destination, "w+", encoding="utf-8") as out_file:
|
||||
out_file.write(data)
|
||||
|
||||
def _runcmd(self, cmd):
|
||||
self.__logger.debug("Running: %s", cmd)
|
||||
|
||||
ret = subprocess.run(cmd, check=True,capture_output=True, encoding='utf-8')
|
||||
return ret.stdout.strip()
|
||||
|
||||
|
||||
def _get_forgejo_data(self):
|
||||
|
||||
data = {
|
||||
"forgejo_user" : self.ssh_user,
|
||||
"forgejo_group" : self.ssh_group,
|
||||
"forgejo_port" : str(self.forgejo_port),
|
||||
"forgejo_bin" : self.forgejo_exe,
|
||||
"forgejo_app_ini" : self.forgejo_ini_file_path,
|
||||
"forgejo_work_path" : self.forgejo_work_dir_path,
|
||||
"forgejo_data_path" : self.forgejo_data_dir_path,
|
||||
"forgejo_db_path" : self.forgejo_db_path,
|
||||
"forgejo_repository_root" : self.git_dir_path,
|
||||
"forgejo_lfs_path" : self.lfs_dir_path,
|
||||
"forgejo_log_path" : self.forgejo_log_dir_path,
|
||||
"forgejo_hostname" : self._runcmd("hostname"),
|
||||
"forgejo_lfs_jwt_secret" : self._runcmd([self.forgejo_exe,"generate", "secret", "LFS_JWT_SECRET"]),
|
||||
"forgejo_jwt_secret" : self._runcmd([self.forgejo_exe,"generate", "secret", "JWT_SECRET"]),
|
||||
"forgejo_internal_token" : self._runcmd([self.forgejo_exe,"generate", "secret", "INTERNAL_TOKEN"]),
|
||||
"forgejo_secret_key" : self._runcmd([self.forgejo_exe,"generate", "secret", "SECRET_KEY"])
|
||||
}
|
||||
|
||||
return data
|
||||
|
||||
def run_forge_cmd(self, args, ignore_errors = None):
|
||||
"""
|
||||
Executes a Forgejo command with the specified arguments and handles errors.
|
||||
|
||||
Args:
|
||||
args (list): List of command-line arguments to pass to the Forgejo executable.
|
||||
ignore_errors (list, optional): List of error message substrings. If any of these are found in the command's stderr output, the error is ignored and the command's stdout is returned. Defaults to an empty list.
|
||||
|
||||
Returns:
|
||||
str: The standard output of the command if it succeeds or if an ignored error occurs.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the command fails and the error is not in the ignore_errors list.
|
||||
|
||||
Logs:
|
||||
- Info: The command being run.
|
||||
- Error: If the command fails, logs the command, return code, stdout, and stderr.
|
||||
- Info: If an error is ignored, logs that the error is being ignored.
|
||||
"""
|
||||
|
||||
if ignore_errors is None:
|
||||
ignore_errors = []
|
||||
|
||||
cmd = [self.forgejo_exe, "--config", self.forgejo_ini_file_path] + args
|
||||
self.__logger.info("Running command: %s", cmd)
|
||||
|
||||
ret = subprocess.run(cmd, check=False, capture_output=True, encoding='utf-8', user=self.ssh_user)
|
||||
if ret.returncode == 0:
|
||||
return ret.stdout.strip()
|
||||
else:
|
||||
self.__logger.error("Failed to run command: %s, return code %i", cmd, ret.returncode)
|
||||
self.__logger.error("stdout: %s", ret.stdout.strip())
|
||||
self.__logger.error("stderr: %s", ret.stderr.strip())
|
||||
|
||||
for err in ignore_errors:
|
||||
if err in ret.stderr:
|
||||
self.__logger.info("Ignoring error, it's in the ignore list")
|
||||
return ret.stdout.strip()
|
||||
|
||||
raise RuntimeError("Failed to run necessary command")
|
||||
|
||||
def configure_forgejo(self):
|
||||
"""
|
||||
Configures the Forgejo service for OpenGnsys.
|
||||
|
||||
This method performs the following steps:
|
||||
1. Retrieves Forgejo configuration data.
|
||||
2. Creates necessary directories for configuration, data, logs, and workspaces.
|
||||
3. Sets ownership of key directories to the SSH user and group.
|
||||
4. Installs configuration and systemd service templates.
|
||||
5. Reloads systemd, enables, and restarts the Forgejo service.
|
||||
6. Waits for the Forgejo service to become available on the configured port.
|
||||
7. Runs Forgejo administrative commands to:
|
||||
- Migrate the database.
|
||||
- Perform a health check.
|
||||
- Create the admin user (if not already present).
|
||||
- Generate an access token for the admin user (if not already present).
|
||||
8. Stores the generated access token in a configuration file, or retains the old token if generation fails.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If any required Forgejo administrative command fails and the error is not in the ignore list.
|
||||
"""
|
||||
# Make sure any custom config from debconf is accounted for
|
||||
self._apply_configuration()
|
||||
|
||||
data = self._get_forgejo_data()
|
||||
self.__logger.debug("Creating directories")
|
||||
|
||||
pathlib.Path(self.opengnsys_etc_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.forgejo_conf_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.git_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.lfs_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.forgejo_work_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.forgejo_work_custom_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.forgejo_data_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.forgejo_db_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
pathlib.Path(self.forgejo_log_dir_path).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
os.chown(self.lfs_dir_path, self.ssh_uid, self.ssh_gid)
|
||||
os.chown(self.git_dir_path, self.ssh_uid, self.ssh_gid)
|
||||
os.chown(self.forgejo_data_dir_path, self.ssh_uid, self.ssh_gid)
|
||||
os.chown(self.forgejo_work_dir_path, self.ssh_uid, self.ssh_gid)
|
||||
os.chown(self.forgejo_db_dir_path, self.ssh_uid, self.ssh_gid)
|
||||
os.chown(self.forgejo_log_dir_path, self.ssh_uid, self.ssh_gid)
|
||||
|
||||
self._install_template(os.path.join(self.template_path, "forgejo-app.ini"), self.forgejo_ini_file_path, data)
|
||||
self._install_template(os.path.join(self.template_path, "opengnsys-forgejo.service"), "/etc/systemd/system/opengnsys-forgejo.service", data)
|
||||
|
||||
self.__logger.debug("Reloading systemd and starting service")
|
||||
subprocess.run(["systemctl", "daemon-reload"], check=True)
|
||||
subprocess.run(["systemctl", "enable", "opengnsys-forgejo"], check=True)
|
||||
subprocess.run(["systemctl", "restart", "opengnsys-forgejo"], check=True)
|
||||
|
||||
self.__logger.info("Waiting for forgejo to start")
|
||||
self._wait_for_port("localhost", self.forgejo_port)
|
||||
|
||||
|
||||
self.__logger.info("Configuring forgejo")
|
||||
|
||||
self.run_forge_cmd(["migrate"])
|
||||
self.run_forge_cmd(["admin", "doctor", "check"])
|
||||
self.run_forge_cmd(["admin", "user", "create", "--username", self.forgejo_user, "--password", self.forgejo_password, "--email", self.email], ignore_errors=["user already exists"])
|
||||
token = self.run_forge_cmd(["admin", "user", "generate-access-token", "--username", self.forgejo_user, "-t", "gitapi", "--scopes", "all", "--raw"], ignore_errors = ["access token name has been used already"])
|
||||
|
||||
if token:
|
||||
with open(os.path.join(self.base_path, "etc", "ogGitApiToken.cfg"), "w+", encoding='utf-8') as token_file:
|
||||
token_file.write(token)
|
||||
else:
|
||||
self.__logger.info("Keeping the old token")
|
||||
|
||||
|
||||
self.__logger.info("Adding default SSH key")
|
||||
with open("/usr/share/opengnsys-forgejo/opengnsys.pub", 'r', encoding='utf-8') as file:
|
||||
ssh_key_data = file.read()
|
||||
|
||||
self.add_forgejo_sshkey(ssh_key_data, "default key")
|
||||
|
||||
|
||||
def add_forgejo_repo(self, repository_name, description = ""):
|
||||
"""
|
||||
Create a new repository in the local Forgejo instance.
|
||||
|
||||
This method sends a POST request to the Forgejo API to create a new repository
|
||||
with the specified name and optional description. The API token is read from
|
||||
the configuration file 'ogGitApiToken.cfg'.
|
||||
|
||||
Args:
|
||||
repository_name (str): The name of the repository to create.
|
||||
description (str, optional): A description for the repository. Defaults to "".
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the HTTP request to Forgejo fails.
|
||||
|
||||
Logs:
|
||||
Information about the repository creation request and its response status.
|
||||
"""
|
||||
token = ""
|
||||
with open(os.path.join(self.base_path, "etc", "ogGitApiToken.cfg"), "r", encoding='utf-8') as token_file:
|
||||
token = token_file.read().strip()
|
||||
|
||||
self.__logger.info("Adding repository %s for Forgejo", repository_name)
|
||||
|
||||
r = requests.post(
|
||||
f"http://localhost:{self.forgejo_port}/api/v1/user/repos",
|
||||
json={
|
||||
"auto_init" : False,
|
||||
"default_branch" : "main",
|
||||
"description" : description,
|
||||
"name" : repository_name,
|
||||
"private" : False
|
||||
}, headers={
|
||||
'Authorization' : f"token {token}"
|
||||
},
|
||||
timeout = 60
|
||||
)
|
||||
|
||||
self.__logger.info("Request status was %i, content %s", r.status_code, r.content)
|
||||
|
||||
def add_forgejo_sshkey(self, pubkey, description = ""):
|
||||
"""
|
||||
Adds an SSH public key to the Forgejo server for the current user.
|
||||
|
||||
This method reads an API token from the configuration file and sends a POST request
|
||||
to the Forgejo API to add the specified SSH public key with an optional description.
|
||||
|
||||
Args:
|
||||
pubkey (str): The SSH public key to be added.
|
||||
description (str, optional): A description or title for the SSH key. Defaults to "".
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the HTTP status code (int) and the response content (str).
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the HTTP request fails.
|
||||
FileNotFoundError: If the API token configuration file does not exist.
|
||||
Exception: For any other unexpected errors.
|
||||
"""
|
||||
token = ""
|
||||
with open(os.path.join(self.base_path, "etc", "ogGitApiToken.cfg"), "r", encoding='utf-8') as token_file:
|
||||
token = token_file.read().strip()
|
||||
|
||||
self.__logger.info("Adding SSH key to Forgejo: %s (%s)", pubkey, description)
|
||||
|
||||
r = requests.post(
|
||||
f"http://localhost:{self.forgejo_port}/api/v1/user/keys",
|
||||
json={
|
||||
"key" : pubkey,
|
||||
"read_only" : False,
|
||||
"title" : description
|
||||
}, headers={
|
||||
'Authorization' : f"token {token}"
|
||||
},
|
||||
timeout = 60
|
||||
)
|
||||
|
||||
self.__logger.info("Request status was %i, content %s", r.status_code, r.content)
|
||||
return r.status_code, r.content.decode('utf-8')
|
||||
|
||||
def add_forgejo_organization(self, pubkey, description = ""):
|
||||
"""
|
||||
Adds an SSH public key to a Forgejo instance for the current user.
|
||||
|
||||
This method reads an API token from a configuration file, then sends a POST request
|
||||
to the Forgejo API to add the provided SSH public key. The key can be given a description,
|
||||
which is used as its title in Forgejo.
|
||||
|
||||
Args:
|
||||
pubkey (str): The SSH public key to add.
|
||||
description (str, optional): A description or title for the key. Defaults to "".
|
||||
|
||||
Logs:
|
||||
- Information about the key being added.
|
||||
- The status code and content of the API response.
|
||||
|
||||
Raises:
|
||||
requests.RequestException: If the HTTP request fails or times out.
|
||||
FileNotFoundError: If the API token configuration file does not exist.
|
||||
Exception: For other unexpected errors.
|
||||
"""
|
||||
token = ""
|
||||
with open(os.path.join(self.base_path, "etc", "ogGitApiToken.cfg"), "r", encoding='utf-8') as token_file:
|
||||
token = token_file.read().strip()
|
||||
|
||||
self.__logger.info("Adding SSH key to Forgejo: %s", pubkey)
|
||||
|
||||
r = requests.post(
|
||||
f"http://localhost:{self.forgejo_port}/api/v1/user/keys",
|
||||
json={
|
||||
"key" : pubkey,
|
||||
"read_only" : False,
|
||||
"title" : description
|
||||
}, headers={
|
||||
'Authorization' : f"token {token}"
|
||||
},
|
||||
timeout = 60
|
||||
)
|
||||
|
||||
self.__logger.info("Request status was %i, content %s", r.status_code, r.content)
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.stdout.reconfigure(encoding='utf-8')
|
||||
|
||||
opengnsys_log_dir = "/opt/opengnsys/log"
|
||||
|
||||
logger = logging.getLogger(__package__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
streamLog = logging.StreamHandler()
|
||||
streamLog.setLevel(logging.INFO)
|
||||
|
||||
pathlib.Path(opengnsys_log_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
logFilePath = f"{opengnsys_log_dir}/git_installer.log"
|
||||
fileLog = logging.FileHandler(logFilePath)
|
||||
fileLog.setLevel(logging.DEBUG)
|
||||
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)24s - [%(levelname)5s] - %(message)s')
|
||||
|
||||
streamLog.setFormatter(formatter)
|
||||
fileLog.setFormatter(formatter)
|
||||
|
||||
logger.addHandler(streamLog)
|
||||
logger.addHandler(fileLog)
|
||||
|
||||
|
||||
|
||||
logger.info("Running post-install script")
|
||||
installer=OpengnsysForgejoPostInstall()
|
||||
|
||||
logger.debug("Obtaining configuration from debconf")
|
||||
|
||||
with debconf.Debconf(run_frontend=True) as db:
|
||||
def dbget(value):
|
||||
ret = db.get(value)
|
||||
logger.debug("Retrieved %s from debconf: %s", value, ret)
|
||||
return ret
|
||||
|
||||
installer.forgejo_organization = dbget('opengnsys/forgejo_organization')
|
||||
installer.forgejo_user = dbget('opengnsys/forgejo_user')
|
||||
installer.forgejo_password = dbget('opengnsys/forgejo_password')
|
||||
installer.email = dbget('opengnsys/forgejo_email')
|
||||
installer.forgejo_port = int(dbget('opengnsys/forgejo_port'))
|
||||
|
||||
# Templates get installed here
|
||||
installer.template_path = "/usr/share/opengnsys-forgejo/"
|
||||
installer.configure_forgejo()
|
||||
sys.exit(0)
|
|
@ -1,25 +0,0 @@
|
|||
Template: opengnsys/forgejo_organization
|
||||
Type: string
|
||||
Default: opengnsys
|
||||
Description: Organizacion de Forgejo
|
||||
|
||||
Template: opengnsys/forgejo_user
|
||||
Type: string
|
||||
Default: oggit
|
||||
Description: Usuario de oggit Forgejo
|
||||
|
||||
Template: opengnsys/forgejo_password
|
||||
Type: password
|
||||
Default: opengnsys
|
||||
Description: Password de cuenta de oggit de Forgejo
|
||||
|
||||
Template: opengnsys/forgejo_email
|
||||
Type: string
|
||||
Default: opegnsys@opengnsys.com
|
||||
Description: Email de cuenta de oggit de Forgejo
|
||||
|
||||
Template: opengnsys/forgejo_port
|
||||
Type: string
|
||||
Default: 3100
|
||||
Description: Puerto TCP de Forgejo
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
data = {
|
||||
"forgejo_user" : self.ssh_user,
|
||||
"forgejo_group" : self.ssh_group,
|
||||
"forgejo_port" : str(self.forgejo_port),
|
||||
"forgejo_bin" : bin_path,
|
||||
"forgejo_app_ini" : conf_path,
|
||||
"forgejo_work_path" : forgejo_work_dir_path,
|
||||
"forgejo_data_path" : forgejo_data_dir_path,
|
||||
"forgejo_db_path" : forgejo_db_path,
|
||||
"forgejo_repository_root" : git_dir_path,
|
||||
"forgejo_lfs_path" : lfs_dir_path,
|
||||
"forgejo_log_path" : forgejo_log_dir_path,
|
||||
"forgejo_hostname" : _runcmd("hostname"),
|
||||
"forgejo_lfs_jwt_secret" : _runcmd([bin_path,"generate", "secret", "LFS_JWT_SECRET"]),
|
||||
"forgejo_jwt_secret" : _runcmd([bin_path,"generate", "secret", "JWT_SECRET"]),
|
||||
"forgejo_internal_token" : _runcmd([bin_path,"generate", "secret", "INTERNAL_TOKEN"]),
|
||||
"forgejo_secret_key" : _runcmd([bin_path,"generate", "secret", "SECRET_KEY"])
|
||||
}
|
||||
|
||||
ini_template = "/usr/share/opengnsys-forgejo/forgejo-app.ini"
|
||||
|
||||
|
||||
def _install_template(self, template, destination, keysvalues):
|
||||
data = ""
|
||||
with open(template, "r", encoding="utf-8") as template_file:
|
||||
data = template_file.read()
|
||||
|
||||
for key in keysvalues.keys():
|
||||
if isinstance(keysvalues[key], int):
|
||||
data = data.replace("{" + key + "}", str(keysvalues[key]))
|
||||
else:
|
||||
data = data.replace("{" + key + "}", keysvalues[key])
|
||||
|
||||
with open(destination, "w+", encoding="utf-8") as out_file:
|
||||
out_file.write(data)
|
||||
|
||||
|
||||
|
||||
|
||||
_install_template(os.path.join(self.script_path, "forgejo-app.ini"), conf_path, data)
|
||||
|
|
@ -22,12 +22,15 @@
|
|||
%:
|
||||
dh $@
|
||||
|
||||
# Ejecutar composer install durante la fase de construcción
|
||||
override_dh_auto_build:
|
||||
cp -v ../../installer/opengnsys_git_installer.py debian/opengnsys-forgejo.postinst
|
||||
|
||||
override_dh_auto_install:
|
||||
dh_auto_install
|
||||
mkdir -p debian/opengnsys-forgejo/opt/opengnsys/ogrepository/var/lib/forgejo
|
||||
mkdir -p debian/opengnsys-forgejo/opt/opengnsys/ogrepository/var/lib/forgejo/work
|
||||
# fails under fakeroot for some reason, fix in postinst
|
||||
# chown -R oggit:oggit debian/opengnsys-forgejo/opt/opengnsys/ogrepository/var/lib/forgejo
|
||||
chown -R oggit:oggit debian/opengnsys-forgejo/opt/opengnsys/ogrepository/var/lib/forgejo
|
||||
|
||||
|
||||
# dh_make generated override targets.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
#!/bin/bash
|
||||
VERSION=11.0.2
|
||||
VERSION=10.0.1
|
||||
|
||||
wget https://codeberg.org/forgejo/forgejo/releases/download/v${VERSION}/forgejo-${VERSION}-linux-amd64 -O forgejo
|
||||
chmod 755 forgejo
|
||||
|
|
|
@ -4,7 +4,7 @@ Type=simple
|
|||
User=oggit
|
||||
Group=oggit
|
||||
WorkingDirectory=/opt/opengnsys/ogrepository/var/lib/forgejo/work
|
||||
ExecStart=/opt/opengnsys/ogrepository/bin/forgejo web --config /opt/opengnsys/ogrepository/etc/forgejo/app.ini
|
||||
ExecStart=/opt/opengnsys/ogrepository/bin/forgejo web --config /opt/opengnsys/etc/forgejo/app.ini
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGztZurO2rmLE8YUv7UOVNrRPpBQEyIurorFmcInmuh9 noname
|
Loading…
Reference in New Issue