diff --git a/api/gitapi.py b/api/gitapi.py old mode 100644 new mode 100755 index d54413d..7f1372a --- a/api/gitapi.py +++ b/api/gitapi.py @@ -52,22 +52,34 @@ 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 REPOSITORIES_BASE_PATH = "/opt/opengnsys/images" +start_time = time.time() +tasks = {} + + # 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) -tasks = {} - - def do_repo_backup(repo, params): """ @@ -147,291 +159,332 @@ def do_repo_gc(repo): # 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. +@api.route('/') +class GitLib(Resource): - Returns: - Response: A Flask JSON response containing a welcome message. - """ - return jsonify({ - "message": "OpenGnsys Git API" - }) + @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. + """ + return { + "message": "OpenGnsys Git API" + } -@app.route('/repositories') -def get_repositories(): - """ - Retrieve a list of Git repositories. +@git_ns.route('/oggit/v1/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. + 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. + 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"] - } - """ + 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] + if not os.path.isdir(REPOSITORIES_BASE_PATH): + return jsonify({"error": "Repository storage not found, git functionality may not be installed."}), 500 - repos = repos + [name] + 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] - return jsonify({ - "repositories": repos - }) + repos = repos + [name] -@app.route('/repositories/', methods=['PUT']) -def create_repo(repo): - """ - Create a new Git repository. + return jsonify({ + "repositories": repos + }) - This endpoint creates a new Git repository with the specified name. - If the repository already exists, it returns a status message indicating so. + def post(self): + """ + Create a new Git repository. - Args: - repo (str): The name of the repository to be created. + This endpoint creates a new Git repository with the specified name. + If the repository already exists, it returns a status message indicating so. - 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 + 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 - installer = OpengnsysGitInstaller() - installer.init_git_repo(repo + ".git") + if data is None: + return jsonify({"error" : "Parameters missing"}), 400 + repo = data["name"] - return jsonify({"status": "Repository created"}), 201 + repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git") + if os.path.isdir(repo_path): + return jsonify({"status": "Repository already exists"}), 200 -@app.route('/repositories//sync', methods=['POST']) -def sync_repo(repo): - """ - Synchronize a repository with a remote repository. + installer = OpengnsysGitInstaller() + installer.init_git_repo(repo + ".git") - 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. + return jsonify({"status": "Repository created"}), 201 - 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 +@git_ns.route('/oggit/v1/repositories//sync') +class GitRepoSync(Resource): + def post(self, repo): + """ + Synchronize a repository with a remote repository. - data = request.json + This endpoint triggers the synchronization process for a specified repository. + It expects a JSON payload with the remote repository details. - if data is None: - return jsonify({"error" : "Parameters missing"}), 400 + Args: + repo (str): The name of the repository to be synchronized. - 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 + 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 -@app.route('/repositories//backup', methods=['POST']) -def backup_repository(repo): - """ - Backup a specified repository. + data = request.json - Endpoint: POST /repositories//backup + if data is None: + return jsonify({"error" : "Parameters missing"}), 400 - Args: - repo (str): The name of the repository to back up. + 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 - 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 +@git_ns.route('/oggit/v1/repositories//backup') +class GitRepoBackup(Resource): + def backup_repository(self, repo): + """ + Backup a specified repository. + Endpoint: POST /repositories//backup - data = request.json - if data is None: - return jsonify({"error" : "Parameters missing"}), 400 + 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. - if not "ssh_port" in data: - data["ssh_port"] = 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 - 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 + data = request.json + if data is None: + return jsonify({"error" : "Parameters missing"}), 400 -@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. + if not "ssh_port" in data: + data["ssh_port"] = 22 - 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_backup, repo, data) + task_id = str(uuid.uuid4()) + tasks[task_id] = future - 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 - return jsonify({"status": "started", "task_id" : task_id}), 200 +@git_ns.route('/oggit/v1/repositories//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. -@app.route('/tasks//status') -def tasks_status(task_id): - """ - Endpoint to check the status of a specific task. + Args: + repo (str): The name of the repository to perform GC on. - Args: - task_id (str): The unique identifier of the task. + 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 - 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 = executor.submit(do_repo_gc, repo) + task_id = str(uuid.uuid4()) + tasks[task_id] = future - future = tasks[task_id] + return jsonify({"status": "started", "task_id" : task_id}), 200 - if future.done(): - result = future.result() - return jsonify({"status" : "completed", "result" : result}), 200 - else: - return jsonify({"status" : "in progress"}), 202 +@git_ns.route('/oggit/v1/tasks//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. -@app.route('/repositories/', methods=['DELETE']) -def delete_repo(repo): - """ - Deletes a Git repository. + 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 - 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. + future = tasks[task_id] - Args: - repo (str): The name of the repository to delete. + if future.done(): + result = future.result() + return jsonify({"status" : "completed", "result" : result}), 200 + else: + return jsonify({"status" : "in progress"}), 202 - 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 +@git_ns.route('/oggit/v1/repositories/', 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): + return jsonify({"error": "Repository not found"}), 404 -@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. + shutil.rmtree(repo_path) + return jsonify({"status": "Repository deleted"}), 200 - 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 - git_repo = git.Repo(repo_path) - branches = [] - for branch in git_repo.branches: - branches = branches + [branch.name] +@git_ns.route('/oggit/v1/repositories//branches') +class GitRepoBranches(Resource): + def get(self, repo): + """ + Retrieve the list of branches for a given repository. - return jsonify({ - "branches": branches - }) + 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 + git_repo = git.Repo(repo_path) + branches = [] + for branch in git_repo.branches: + branches = branches + [branch.name] -@app.route('/health') -def health_check(): - """ - Health check endpoint. - This endpoint returns a JSON response indicating the health status of the application. + return jsonify({ + "branches": branches + }) + + + + +@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. + + """ + 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 + + """ + return { + "uptime" : time.time() - start_time, + "active_tasks" : len(tasks) + } + + +api.add_namespace(git_ns) - 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__': + print(f"Map: {app.url_map}") app.run(debug=True, host='0.0.0.0') diff --git a/api/requirements-3.13.txt b/api/requirements-3.13.txt new file mode 100644 index 0000000..eb57d6f --- /dev/null +++ b/api/requirements-3.13.txt @@ -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