from flask import Flask, jsonify import os.path import os import git import shutil import subprocess import uuid from opengnsys_git_installer import OpengnsysGitInstaller from flask import Flask, request from flask_executor import Executor import subprocess from flask import stream_with_context, Response import paramiko repositories_base_path = "/opt/opengnsys/images" # Create an instance of the Flask class app = Flask(__name__) executor = Executor(app) tasks = {} def do_repo_backup(repo, params): gitrepo = git.Repo(f"{repositories_base_path}/{repo}.git") 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: gitrepo.archive(remote_file, format="tar.gz") return True def do_repo_sync(repo, params): gitrepo = git.Repo(f"{repositories_base_path}/{repo}.git") # Recreate the remote every time, it might change if "backup" in gitrepo.remotes: gitrepo.delete_remote("backup") backup_repo = gitrepo.create_remote("backup", params["remote_repository"]) pushrets = backup_repo.push("*:*") results = [] # This gets returned to the API for ret in pushrets: results = results + [ {"local_ref" : ret.local_ref.name, "remote_ref" : ret.remote_ref.name, "summary" : ret.summary }] return results def do_repo_gc(repo): gitrepo = git.Repo(f"{repositories_base_path}/{repo}.git") gitrepo.git.gc() return True # Define a route for the root URL @app.route('/') def home(): """ 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. """ return jsonify({ "message": "OpenGnsys Git API" }) @app.route('/repositories') def get_repositories(): """ 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): return jsonify({"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] return jsonify({ "repositories": repos }) @app.route('/repositories/', methods=['PUT']) def create_repo(repo): """ 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. """ repo_path = os.path.join(repositories_base_path, repo + ".git") if os.path.isdir(repo_path): return jsonify({"status": "Repository already exists"}), 200 installer = OpengnsysGitInstaller() installer._init_git_repo(repo + ".git") return jsonify({"status": "Repository created"}), 201 @app.route('/repositories//sync', methods=['POST']) def sync_repo(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): return jsonify({"error": "Repository not found"}), 404 data = request.json if data is None: return jsonify({"error" : "Parameters missing"}), 400 dest_repo = data["remote_repository"] future = executor.submit(do_repo_sync, repo, data) task_id = str(uuid.uuid4()) tasks[task_id] = future return jsonify({"status": "started", "task_id" : task_id}), 200 @app.route('/repositories//backup', methods=['POST']) def backup_repo(repo): """ Backup a specified repository. Endpoint: POST /repositories//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): return jsonify({"error": "Repository not found"}), 404 data = request.json if data is None: return jsonify({"error" : "Parameters missing"}), 400 if not "ssh_port" in data: data["ssh_port"] = 22 future = executor.submit(do_repo_backup, repo, data) task_id = str(uuid.uuid4()) tasks[task_id] = future return jsonify({"status": "started", "task_id" : task_id}), 200 @app.route('/repositories//gc', methods=['POST']) def gc_repo(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): return jsonify({"error": "Repository not found"}), 404 future = executor.submit(do_repo_gc, repo) task_id = str(uuid.uuid4()) tasks[task_id] = future return jsonify({"status": "started", "task_id" : task_id}), 200 @app.route('/tasks//status') def tasks_status(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: return jsonify({"error": "Task not found"}), 404 future = tasks[task_id] if future.done(): result = future.result() return jsonify({"status" : "completed", "result" : result}), 200 else: return jsonify({"status" : "in progress"}), 202 @app.route('/repositories/', methods=['DELETE']) def delete_repo(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): return jsonify({"error": "Repository not found"}), 404 shutil.rmtree(repo_path) return jsonify({"status": "Repository deleted"}), 200 @app.route('/repositories//branches') def get_repository_branches(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): return jsonify({"error": "Repository not found"}), 404 gitRepo = git.Repo(repo_path) branches = [] for branch in gitRepo.branches: branches = branches + [branch.name] return jsonify({ "branches": branches }) @app.route('/health') def health_check(): """ 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. """ return jsonify({ "status": "OK" }) # Run the Flask app if __name__ == '__main__': app.run(debug=True, host='0.0.0.0')