Ticket #808: Initial implementation

fixes
Vadim vtroshchinskiy 2024-10-11 23:34:53 +02:00
parent aae1f3a1b7
commit 9a0faff058
2 changed files with 298 additions and 211 deletions

475
api/gitapi.py 100644 → 100755
View File

@ -52,22 +52,34 @@ import os
import shutil import shutil
import uuid import uuid
import git import git
import time
from opengnsys_git_installer import OpengnsysGitInstaller from opengnsys_git_installer import OpengnsysGitInstaller
from flask import Flask, request, jsonify # stream_with_context, Response, from flask import Flask, request, jsonify # stream_with_context, Response,
from flask_executor import Executor from flask_executor import Executor
from flask_restx import Api, Resource, fields
#from flasgger import Swagger
import paramiko import paramiko
REPOSITORIES_BASE_PATH = "/opt/opengnsys/images" REPOSITORIES_BASE_PATH = "/opt/opengnsys/images"
start_time = time.time()
tasks = {}
# Create an instance of the Flask class # Create an instance of the Flask class
app = Flask(__name__) 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) executor = Executor(app)
tasks = {}
def do_repo_backup(repo, params): def do_repo_backup(repo, params):
""" """
@ -147,291 +159,332 @@ def do_repo_gc(repo):
# Define a route for the root URL # Define a route for the root URL
@app.route('/') @api.route('/')
def home(): class GitLib(Resource):
"""
Home route that returns a JSON response with a welcome message for the OpenGnsys Git API.
Returns: @api.doc('home')
Response: A Flask JSON response containing a welcome message. def get(self):
""" """
return jsonify({ Home route that returns a JSON response with a welcome message for the OpenGnsys Git API.
"message": "OpenGnsys Git API"
}) Returns:
Response: A Flask JSON response containing a welcome message.
"""
return {
"message": "OpenGnsys Git API"
}
@app.route('/repositories') @git_ns.route('/oggit/v1/repositories')
def get_repositories(): class GitRepositories(Resource):
""" def get(self):
Retrieve a list of Git repositories. """
Retrieve a list of Git repositories.
This endpoint scans the OpenGnsys image path for directories that This endpoint scans the OpenGnsys image path for directories that
appear to be Git repositories (i.e., they contain a "HEAD" file). appear to be Git repositories (i.e., they contain a "HEAD" file).
It returns a JSON response containing the names of these repositories. It returns a JSON response containing the names of these repositories.
Returns: Returns:
Response: A JSON response with a list of repository names or an Response: A JSON response with a list of repository names or an
error message if the repository storage is not found. error message if the repository storage is not found.
- 200 OK: When the repositories are successfully retrieved. - 200 OK: When the repositories are successfully retrieved.
- 500 Internal Server Error: When the repository storage is not found. - 500 Internal Server Error: When the repository storage is not found.
Example JSON response: Example JSON response:
{ {
"repositories": ["repo1", "repo2"] "repositories": ["repo1", "repo2"]
} }
""" """
if not os.path.isdir(REPOSITORIES_BASE_PATH): if not os.path.isdir(REPOSITORIES_BASE_PATH):
return jsonify({"error": "Repository storage not found, git functionality may not be installed."}), 500 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] 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({ repos = repos + [name]
"repositories": repos
})
@app.route('/repositories/<repo>', methods=['PUT']) return jsonify({
def create_repo(repo): "repositories": repos
""" })
Create a new Git repository.
This endpoint creates a new Git repository with the specified name. def post(self):
If the repository already exists, it returns a status message indicating so. """
Create a new Git repository.
Args: This endpoint creates a new Git repository with the specified name.
repo (str): The name of the repository to be created. If the repository already exists, it returns a status message indicating so.
Returns: Args:
Response: A JSON response with a status message and HTTP status code. repo (str): The name of the repository to be created.
- 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
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() if data is None:
installer.init_git_repo(repo + ".git") 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/<repo>/sync', methods=['POST']) installer = OpengnsysGitInstaller()
def sync_repo(repo): installer.init_git_repo(repo + ".git")
"""
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: return jsonify({"status": "Repository created"}), 201
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
@git_ns.route('/oggit/v1/repositories/<repo>/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: Args:
return jsonify({"error" : "Parameters missing"}), 400 repo (str): The name of the repository to be synchronized.
future = executor.submit(do_repo_sync, repo, data) Returns:
task_id = str(uuid.uuid4()) Response: A JSON response indicating the status of the synchronization process.
tasks[task_id] = future - 200: If the synchronization process has started successfully.
return jsonify({"status": "started", "task_id" : task_id}), 200 - 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/<repo>/backup', methods=['POST']) data = request.json
def backup_repository(repo):
"""
Backup a specified repository.
Endpoint: POST /repositories/<repo>/backup if data is None:
return jsonify({"error" : "Parameters missing"}), 400
Args: future = executor.submit(do_repo_sync, repo, data)
repo (str): The name of the repository to back up. 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: @git_ns.route('/oggit/v1/repositories/<repo>/backup')
- The repository path is constructed by appending ".git" to the repository name. class GitRepoBackup(Resource):
- The backup operation is performed asynchronously using a thread pool executor. def backup_repository(self, repo):
- The task ID of the backup operation is generated using UUID and stored in a global tasks dictionary. """
""" Backup a specified repository.
repo_path = os.path.join(REPOSITORIES_BASE_PATH, repo + ".git")
if not os.path.isdir(repo_path):
return jsonify({"error": "Repository not found"}), 404
Endpoint: POST /repositories/<repo>/backup
data = request.json Args:
if data is None: repo (str): The name of the repository to back up.
return jsonify({"error" : "Parameters missing"}), 400
Request Body (JSON):
ssh_port (int, optional): The SSH port to use for the backup. Defaults to 22.
if not "ssh_port" in data: Returns:
data["ssh_port"] = 22 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/<repo>/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. if not "ssh_port" in data:
The task is submitted to an executor, and a unique task ID is generated data["ssh_port"] = 22
and returned to the client.
Args:
repo (str): The name of the repository to perform GC on.
Returns: future = executor.submit(do_repo_backup, repo, data)
Response: A JSON response containing the status of the request and task_id = str(uuid.uuid4())
a unique task ID if the repository is found, or an error tasks[task_id] = future
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) return jsonify({"status": "started", "task_id" : task_id}), 200
task_id = str(uuid.uuid4())
tasks[task_id] = future
return jsonify({"status": "started", "task_id" : task_id}), 200 @git_ns.route('/oggit/v1/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.
@app.route('/tasks/<task_id>/status') Args:
def tasks_status(task_id): repo (str): The name of the repository to perform GC on.
"""
Endpoint to check the status of a specific task.
Args: Returns:
task_id (str): The unique identifier of the task. 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: future = executor.submit(do_repo_gc, repo)
Response: A JSON response containing the status of the task. task_id = str(uuid.uuid4())
- If the task is not found, returns a 404 error with an error message. tasks[task_id] = future
- 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] 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/<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.
@app.route('/repositories/<repo>', methods=['DELETE']) Returns:
def delete_repo(repo): Response: A JSON response containing the status of the task.
""" - If the task is not found, returns a 404 error with an error message.
Deletes a Git repository. - 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. future = tasks[task_id]
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: if future.done():
repo (str): The name of the repository to delete. 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) @git_ns.route('/oggit/v1/repositories/<repo>', methods=['DELETE'])
return jsonify({"status": "Repository deleted"}), 200 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/<repo>/branches')
def get_repository_branches(repo):
"""
Retrieve the list of branches for a given repository.
Args: shutil.rmtree(repo_path)
repo (str): The name of the repository. 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/<repo>/branches')
class GitRepoBranches(Resource):
def get(self, repo):
"""
Retrieve the list of branches for a given repository.
return jsonify({ Args:
"branches": branches 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 # Run the Flask app
if __name__ == '__main__': if __name__ == '__main__':
print(f"Map: {app.url_map}")
app.run(debug=True, host='0.0.0.0') app.run(debug=True, host='0.0.0.0')

View File

@ -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