feat: Initial commit for Flask API
						commit
						8afb8593d6
					
				|  | @ -0,0 +1,132 @@ | |||
| # Byte-compiled / optimized / DLL files | ||||
| __pycache__/ | ||||
| *.py[cod] | ||||
| *$py.class | ||||
| 
 | ||||
| # C extensions | ||||
| *.so | ||||
| 
 | ||||
| # Distribution / packaging | ||||
| .Python | ||||
| build/ | ||||
| develop-eggs/ | ||||
| dist/ | ||||
| downloads/ | ||||
| eggs/ | ||||
| .eggs/ | ||||
| lib/ | ||||
| lib64/ | ||||
| parts/ | ||||
| sdist/ | ||||
| var/ | ||||
| wheels/ | ||||
| pip-wheel-metadata/ | ||||
| share/python-wheels/ | ||||
| *.egg-info/ | ||||
| .installed.cfg | ||||
| *.egg | ||||
| MANIFEST | ||||
| 
 | ||||
| # PyInstaller | ||||
| #  Usually these files are written by a python script from a template | ||||
| #  before PyInstaller builds the exe, so as to inject date/other infos into it. | ||||
| *.manifest | ||||
| *.spec | ||||
| 
 | ||||
| # Installer logs | ||||
| pip-log.txt | ||||
| pip-delete-this-directory.txt | ||||
| 
 | ||||
| # Unit test / coverage reports | ||||
| htmlcov/ | ||||
| .tox/ | ||||
| .nox/ | ||||
| .coverage | ||||
| .coverage.* | ||||
| .cache | ||||
| nosetests.xml | ||||
| coverage.xml | ||||
| *.cover | ||||
| *.py,cover | ||||
| .hypothesis/ | ||||
| .pytest_cache/ | ||||
| 
 | ||||
| # Translations | ||||
| *.mo | ||||
| *.pot | ||||
| 
 | ||||
| # Django stuff: | ||||
| *.log | ||||
| local_settings.py | ||||
| db.sqlite3 | ||||
| db.sqlite3-journal | ||||
| 
 | ||||
| # Flask stuff: | ||||
| instance/ | ||||
| .webassets-cache | ||||
| 
 | ||||
| # Scrapy stuff: | ||||
| .scrapy | ||||
| 
 | ||||
| # Sphinx documentation | ||||
| docs/_build/ | ||||
| 
 | ||||
| # PyBuilder | ||||
| target/ | ||||
| 
 | ||||
| # Jupyter Notebook | ||||
| .ipynb_checkpoints | ||||
| 
 | ||||
| # IPython | ||||
| profile_default/ | ||||
| ipython_config.py | ||||
| 
 | ||||
| # pyenv | ||||
| .python-version | ||||
| 
 | ||||
| # pipenv | ||||
| #   According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. | ||||
| #   However, in case of collaboration, if having platform-specific dependencies or dependencies | ||||
| #   having no cross-platform support, pipenv may install dependencies that don't work, or not | ||||
| #   install all needed dependencies. | ||||
| #Pipfile.lock | ||||
| 
 | ||||
| # PEP 582; used by e.g. github.com/David-OConnor/pyflow | ||||
| __pypackages__/ | ||||
| 
 | ||||
| # Celery stuff | ||||
| celerybeat-schedule | ||||
| celerybeat.pid | ||||
| 
 | ||||
| # SageMath parsed files | ||||
| *.sage.py | ||||
| 
 | ||||
| # Environments | ||||
| .env | ||||
| .venv | ||||
| env/ | ||||
| venv/ | ||||
| ENV/ | ||||
| env.bak/ | ||||
| venv.bak/ | ||||
| 
 | ||||
| # Spyder project settings | ||||
| .spyderproject | ||||
| .spyproject | ||||
| 
 | ||||
| # Rope project settings | ||||
| .ropeproject | ||||
| 
 | ||||
| # mkdocs documentation | ||||
| /site | ||||
| 
 | ||||
| # mypy | ||||
| .mypy_cache/ | ||||
| .dmypy.json | ||||
| dmypy.json | ||||
| 
 | ||||
| # Pyre type checker | ||||
| .pyre/ | ||||
| 
 | ||||
| 
 | ||||
| .idea | ||||
|  | @ -0,0 +1,6 @@ | |||
| from app import app | ||||
| from os import environ | ||||
| 
 | ||||
| if __name__ == '__main__': | ||||
|     SERVER_HOST = environ.get('SERVER_HOST', 'localhost') | ||||
|     app.run(host=SERVER_HOST, port=5500, debug=(not environ.get('ENV') == 'DEVELOPMENT'),threaded=True) | ||||
|  | @ -0,0 +1,29 @@ | |||
| from flask import Flask, Blueprint | ||||
| from flask_restplus import Api | ||||
| from werkzeug.middleware.proxy_fix import ProxyFix  | ||||
| from app.controllers.container import api as home_ns  | ||||
| from app.utils.authorization import token_required  | ||||
| 
 | ||||
| app = Flask(__name__) | ||||
| app.wsgi_app = ProxyFix(app.wsgi_app) | ||||
| blueprint = Blueprint('api', __name__) | ||||
| app.register_blueprint(blueprint) | ||||
| 
 | ||||
| authorizations = { | ||||
|     'apikey': { | ||||
|         'name': "X-API-KEY", | ||||
|         'in': "header", | ||||
|         'type': "apiKey", | ||||
|         'description': "Insert your Token here!" | ||||
|     } | ||||
| } | ||||
| api = Api(app, | ||||
|           title='Proxmox container rest API', | ||||
|           version='1.0', | ||||
|           description='The Proxmox Container Management API allows users to interact with Proxmox using its REST API for container management. Key functionalities include creating, updating, editing, deleting, and retrieving information about containers.', | ||||
|           prefix='/api', | ||||
|           authorizations=authorizations, | ||||
|           security='apiKey') | ||||
| 
 | ||||
| 
 | ||||
| api.add_namespace(home_ns, path='/container') | ||||
|  | @ -0,0 +1,268 @@ | |||
| 
 | ||||
| from app.utils.authorization import token_required | ||||
| from flask_restplus import Resource, Namespace, fields | ||||
| from flask import request, jsonify | ||||
| import requests | ||||
| import time | ||||
| from flask_restx import fields | ||||
| from functools import wraps | ||||
| import os | ||||
| from dotenv import load_dotenv | ||||
| load_dotenv() | ||||
| 
 | ||||
| 
 | ||||
| api = Namespace('Container', description='Containers creation and manager') | ||||
| 
 | ||||
| API_BASE_URL = f'https://{os.getenv("PROXMOX_NODE_IP")}:8006/api2/json/nodes/{os.getenv("PROXMOX_NODE_NAME")}/lxc' | ||||
| API_AUTH = f'https://{os.getenv("PROXMOX_NODE_IP")}:8006/api2/json/access/ticket' | ||||
| CREDENTIAL = { | ||||
|     "username": f"{os.getenv('API_USER')}@{os.getenv('PROXMOX_NODE_NAME')}", "password": os.getenv("API_USER_PASSWORD")} | ||||
| 
 | ||||
| 
 | ||||
| def get_data(endpoint, data): | ||||
|     response = requests.post(endpoint, data=data, verify=False) | ||||
|     response.raise_for_status() | ||||
|     return response.json()["data"] | ||||
| 
 | ||||
| 
 | ||||
| # Get ticket and CSRFPreventionToken | ||||
| ticket = get_data(API_AUTH, CREDENTIAL)["ticket"] | ||||
| csrf_token = get_data(API_AUTH, CREDENTIAL)["CSRFPreventionToken"] | ||||
| 
 | ||||
| 
 | ||||
| # Function to retrieve information about all containers | ||||
| def list_all_containers(): | ||||
|     endpoint = f"{API_BASE_URL}" | ||||
|     response = requests.get( | ||||
|         endpoint, cookies={"PVEAuthCookie": ticket}, verify=False) | ||||
|     response.raise_for_status() | ||||
| 
 | ||||
|     containers = response.json()["data"] | ||||
|     return containers | ||||
| 
 | ||||
| 
 | ||||
| # Function to retrieve information about a specific container by ID | ||||
| def get_container_info(container_id): | ||||
|     endpoint = f"{API_BASE_URL}/{container_id}/config" | ||||
|     response = requests.get( | ||||
|         endpoint, cookies={"PVEAuthCookie": ticket}, verify=False) | ||||
|     response.raise_for_status() | ||||
|     is_container_locked(container_id) | ||||
|     container_info = response.json()["data"] | ||||
| 
 | ||||
|     return container_info | ||||
| 
 | ||||
| 
 | ||||
| # Function to check if a container is locked | ||||
| def is_container_locked(container_id): | ||||
|     endpoint = f"{API_BASE_URL}/{container_id}/status/current" | ||||
|     response = requests.get( | ||||
|         endpoint, cookies={"PVEAuthCookie": ticket}, verify=False) | ||||
|     response.raise_for_status() | ||||
|     if 'lock' in response.json()["data"]: | ||||
|         return {"locked": True} | ||||
|     else: | ||||
|         return {"locked": False} | ||||
| 
 | ||||
| 
 | ||||
| # Decorator for handling exceptions in routes | ||||
| def handle_exceptions(func): | ||||
|     @wraps(func) | ||||
|     def decorated_function(*args, **kwargs): | ||||
|         try: | ||||
|             return func(*args, **kwargs) | ||||
|         except requests.HTTPError as e: | ||||
|             return {"error": f"HTTP error occurred: {str(e)}"}, e.response.status_code | ||||
|         except requests.RequestException as e: | ||||
|             return {"error": f"Request error occurred: {str(e)}"}, 500 | ||||
|         except Exception as e: | ||||
|             return {"error": f"Internal server error occurred: {str(e)}"}, 500 | ||||
|     return decorated_function | ||||
| 
 | ||||
| 
 | ||||
| @api.route('/') | ||||
| class ContainerListAll(Resource): | ||||
|     @api.response(200, "Success") | ||||
|     @api.doc('some operation', security='apikey') | ||||
|     @token_required | ||||
|     @handle_exceptions | ||||
|     def get(self): | ||||
|         return list_all_containers(), 200 | ||||
| 
 | ||||
| 
 | ||||
| @api.route('/<id>') | ||||
| class ContainerIdInfo(Resource): | ||||
|     @api.response(200, "Success") | ||||
|     @api.doc('some operation', security='apikey') | ||||
|     @token_required | ||||
|     @api.doc(params={'id': 'The container id'},) | ||||
|     @handle_exceptions | ||||
|     def get(self, id): | ||||
|         return get_container_info(id), 200 | ||||
| 
 | ||||
| 
 | ||||
| @api.route('/<id>/<command>') | ||||
| class ContainerId(Resource): | ||||
|     @api.response(200, "Success") | ||||
|     @api.doc('some operation', security='apikey') | ||||
|     @token_required | ||||
|     @api.doc(params={'id': 'The container id'},) | ||||
|     @api.doc(params={'command': 'start, stop, delete'},) | ||||
|     @handle_exceptions | ||||
|     def get(self, id: int, command: str): | ||||
| 
 | ||||
|         commands = ['start', 'stop', 'delete'] | ||||
| 
 | ||||
|         if (command not in commands): | ||||
|             return {"error": f"Bad Request: Invalid command passed in your route'{command}'. Valid commands are {', '.join(commands)}"}, 400 | ||||
| 
 | ||||
|         if command in ["start", "stop"]: | ||||
|             endpoint = f"{API_BASE_URL}/{id}/status/{command}" | ||||
|             response = requests.post(endpoint, cookies={"PVEAuthCookie": ticket}, headers={ | ||||
|                 "CSRFPreventionToken": csrf_token}, verify=False) | ||||
|             response.raise_for_status() | ||||
| 
 | ||||
|         elif command == "delete": | ||||
|             endpoint = f"{API_BASE_URL}/{id}" | ||||
|             response = requests.delete(endpoint, cookies={"PVEAuthCookie": ticket}, headers={ | ||||
|                 "CSRFPreventionToken": csrf_token}, verify=False) | ||||
|             response.raise_for_status() | ||||
| 
 | ||||
|         return {'message': 'success'}, 200 | ||||
| 
 | ||||
| 
 | ||||
| @api.route('/<id>/edit') | ||||
| class ContainerIdEdit(Resource): | ||||
| 
 | ||||
|     payload_model = api.model('ContainerEditModel', { | ||||
|         "nameserver": fields.String(example="8.8.8.8,4.4.4.4"), | ||||
|         "searchdomain": fields.String(example="hittelco.com.br"), | ||||
|     }) | ||||
| 
 | ||||
|     @api.response(200, "Success") | ||||
|     @api.doc('some operation', security='apikey') | ||||
|     @token_required | ||||
|     @api.doc(params={'id': 'The container id'},) | ||||
|     @api.expect(payload_model, validate=True) | ||||
|     @handle_exceptions | ||||
|     def put(self, id: int): | ||||
|         data = request.json | ||||
|         endpoint = f"{API_BASE_URL}/{id}/config" | ||||
|         response = requests.put(endpoint, cookies={"PVEAuthCookie": ticket}, headers={ | ||||
|                                 "CSRFPreventionToken": csrf_token}, data=data, verify=False) | ||||
|         response.raise_for_status() | ||||
| 
 | ||||
|         return {'message': 'success'}, 200 | ||||
| 
 | ||||
| 
 | ||||
| @api.route('/create/up') | ||||
| class ContainerCreateUp(Resource): | ||||
| 
 | ||||
|     payload_model = api.model('ContainerCreateUpModel', { | ||||
|         "net0": fields.String(example="name=tnetVM_ID,bridge=vmbr0"), | ||||
|         "ostemplate": fields.String(example="local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"), | ||||
|         "storage": fields.String(example="local"), | ||||
|         "cores": fields.String(example="1"), | ||||
|         "cpuunits": fields.String(example="512"), | ||||
|         "memory": fields.String(example="512"), | ||||
|         "swap": fields.String(example="0"), | ||||
|         "password": fields.String(example="91472432"), | ||||
|         "hostname": fields.String(example="ctnodeapi"), | ||||
|         "nameserver": fields.String(example="8.8.8.8,4.4.4.4"), | ||||
|         "searchdomain": fields.String(example="hittelco.com.br"), | ||||
|     }) | ||||
| 
 | ||||
|     @api.response(201, "Success")  | ||||
| 
 | ||||
|     @api.doc('some operation', security='apikey') | ||||
|     @token_required | ||||
|     @api.expect(payload_model, validate=True) | ||||
|     @handle_exceptions | ||||
|     def post(self,): | ||||
|         data = request.json | ||||
| 
 | ||||
|         vm_id = 1 | ||||
| 
 | ||||
|         vm_id_list = sorted( | ||||
|             list(map(lambda x: int(x.get("vmid")), list_all_containers()))) | ||||
|         print('vm_id_list: ', vm_id_list) | ||||
| 
 | ||||
|         if (len(vm_id_list) > 0): | ||||
|             vm_id = vm_id_list[-1] + 1 | ||||
| 
 | ||||
|         for key, value in data.items(): | ||||
|             if key == 'vmid': | ||||
|                 return {"error": f'Bad Request: The "vmid" property is automatically setting'}, 400 | ||||
| 
 | ||||
|             if key in ["cores", "cpuunits", "memory", "swap"]: | ||||
|                 data[key] = int(value) | ||||
| 
 | ||||
|             if key == 'hostname': | ||||
|                 data[key] = value+f'{vm_id}' | ||||
| 
 | ||||
|             if 'VM_ID' in value: | ||||
|                 data[key] = value.replace('VM_ID', f'{vm_id}') | ||||
| 
 | ||||
|         aux_data = { | ||||
|             "vmid": vm_id | ||||
|         } | ||||
| 
 | ||||
|         data = {**data, **aux_data} | ||||
| 
 | ||||
|         # Create container | ||||
|         endpoint = f"{API_BASE_URL}" | ||||
|         response = requests.post(endpoint, cookies={"PVEAuthCookie": ticket}, headers={ | ||||
|             "CSRFPreventionToken": csrf_token}, data=data, verify=False) | ||||
|         response.raise_for_status() | ||||
|         print(f'Container id {vm_id} created!') | ||||
| 
 | ||||
|         # Check each 3 seconds if the container is locked before start | ||||
|         while is_container_locked(vm_id)['locked']: | ||||
|             print('*********** LOCKED ************') | ||||
|             time.sleep(5) | ||||
| 
 | ||||
|         # Start container | ||||
|         endpoint = f"{API_BASE_URL}/{vm_id}/status/start" | ||||
|         response = requests.post(endpoint, cookies={"PVEAuthCookie": ticket}, headers={ | ||||
|             "CSRFPreventionToken": csrf_token}, verify=False) | ||||
|         response.raise_for_status() | ||||
|         print(f'Container id {vm_id} started!') | ||||
| 
 | ||||
|         return {'message': 'success', 'id': vm_id}, 201 | ||||
| 
 | ||||
| 
 | ||||
| @api.route('/create') | ||||
| class ContainerCreate(Resource): | ||||
| 
 | ||||
|     payload_model = api.model('ContainerCreateModel',  { | ||||
|         "net0": fields.String(example="name=tnet538,bridge=vmbr0"), | ||||
|         "ostemplate": fields.String(example="local:vztmpl/ubuntu-22.04-standard_22.04-1_amd64.tar.zst"), | ||||
|         "storage": fields.String(example="local"), | ||||
|         "vmid": fields.String(example="538"), | ||||
|         "cores": fields.String(example="1"), | ||||
|         "cpuunits": fields.String(example="512"), | ||||
|         "memory": fields.String(example="512"), | ||||
|         "swap": fields.String(example="0"), | ||||
|         "password": fields.String(example="988325936"), | ||||
|         "hostname": fields.String(example="ctnodeapi538"), | ||||
|         "nameserver": fields.String(example="8.8.8.8,4.4.4.4"), | ||||
|         "searchdomain": fields.String(example="hittelco.com.br"), | ||||
|     }) | ||||
| 
 | ||||
|     @api.response(201, "Success") | ||||
|     @api.doc('some operation', security='apikey') | ||||
|     @token_required | ||||
|     @api.expect(payload_model, validate=True) | ||||
|     @handle_exceptions | ||||
|     def post(self,): | ||||
|         data = request.json | ||||
| 
 | ||||
|         for key, value in data.items(): | ||||
|             if key in ["cores", "cpuunits", "memory", "swap", "vmid"]: | ||||
|                 data[key] = int(value) | ||||
| 
 | ||||
|         endpoint = f"{API_BASE_URL}" | ||||
|         response = requests.post(endpoint, cookies={"PVEAuthCookie": ticket}, headers={ | ||||
|             "CSRFPreventionToken": csrf_token}, data=data, verify=False) | ||||
|         response.raise_for_status() | ||||
|         return {'message': 'success'}, 201 | ||||
|  | @ -0,0 +1,21 @@ | |||
| from functools import wraps | ||||
| from flask import request  | ||||
| import os | ||||
| 
 | ||||
| 
 | ||||
| def token_required(f): | ||||
|     @wraps(f) | ||||
|     def decorated(*args, **kwargs): | ||||
|         print('request.headers: ', request.headers) | ||||
| 
 | ||||
|         token = None | ||||
|         if 'x-api-key' in request.headers: | ||||
|             token = request.headers['x-api-key'] | ||||
|         if not token: | ||||
|             return {'message': "Token not found"}, 401 | ||||
|         if token != os.getenv('TOKEN'): | ||||
|             return {'message': "Invalid token"}, 401 | ||||
| 
 | ||||
|         return f(*args, **kwargs) | ||||
| 
 | ||||
|     return decorated | ||||
|  | @ -0,0 +1,29 @@ | |||
| aniso8601==8.0.0 | ||||
| attrs>=22.2.0 | ||||
| blinker==1.7.0 | ||||
| certifi==2020.6.20 | ||||
| chardet==3.0.4 | ||||
| click==7.1.2 | ||||
| Flask==1.1.2 | ||||
| flask-restplus==0.13.0 | ||||
| flask-restx==1.3.0 | ||||
| Flask-Testing==0.8.0 | ||||
| idna==2.10 | ||||
| importlib-metadata==1.7.0 | ||||
| importlib-resources==6.1.1 | ||||
| itsdangerous==1.1.0 | ||||
| Jinja2==2.11.2 | ||||
| jsonschema==3.2.0 | ||||
| jsonschema-specifications==2023.12.1 | ||||
| MarkupSafe==1.1.1 | ||||
| pkgutil_resolve_name==1.3.10 | ||||
| pyrsistent==0.16.0 | ||||
| python-dotenv==1.0.0 | ||||
| pytz==2020.1 | ||||
| referencing==0.32.1 | ||||
| requests==2.24.0 | ||||
| rpds-py==0.16.2 | ||||
| six==1.15.0 | ||||
| urllib3==1.25.9 | ||||
| Werkzeug==0.16.1 | ||||
| zipp==3.1.0 | ||||
		Loading…
	
		Reference in New Issue