From ad082eb0f9b80e2a7b9164091d86c90b05da8d83 Mon Sep 17 00:00:00 2001 From: forest Date: Sun, 3 Jan 2021 14:44:56 -0600 Subject: [PATCH] fleshing out spoke API --- capsulflask/hub_model.py | 6 ++- capsulflask/spoke_api.py | 106 ++++++++++++++++++++++++++++++++++++- capsulflask/spoke_model.py | 10 +++- 3 files changed, 117 insertions(+), 5 deletions(-) diff --git a/capsulflask/hub_model.py b/capsulflask/hub_model.py index 3f15896..6489327 100644 --- a/capsulflask/hub_model.py +++ b/capsulflask/hub_model.py @@ -241,6 +241,7 @@ class CapsulFlaskHub(HubInterface): operation_id = op[0] results = op[1] number_of_assigned = 0 + error_message = "" assigned_hosts = [] for i in range(len(results)): host = online_hosts[i] @@ -250,6 +251,8 @@ class CapsulFlaskHub(HubInterface): if isinstance(result_body, dict) and 'assignment_status' in result_body and result_body['assignment_status'] == "assigned": number_of_assigned += 1 assigned_hosts.append(host.id) + if isinstance(result_body, dict) and 'error_message' in result_body: + error_message = result_body['error_message'] except: # no need to do anything here since if it cant be parsed then generic_operation will handle it. pass @@ -257,7 +260,8 @@ class CapsulFlaskHub(HubInterface): if number_of_assigned != 1: assigned_hosts_string = ", ".join(assigned_hosts) raise ValueError(f"expected create capsul operation {operation_id} to be assigned to one host, it was assigned to {number_of_assigned} ({assigned_hosts_string})") - + if error_message != "": + raise ValueError(f"create capsul operation {operation_id} on {assigned_hosts_string} failed with {error_message}") def destroy(self, email: str, id: str): diff --git a/capsulflask/spoke_api.py b/capsulflask/spoke_api.py index 3ae6f1b..cc3f67f 100644 --- a/capsulflask/spoke_api.py +++ b/capsulflask/spoke_api.py @@ -1,11 +1,13 @@ +import sys import aiohttp from flask import Blueprint from flask import current_app from flask import request +from flask.json import jsonify from werkzeug.exceptions import abort -from capsulflask.db import get_model, my_exec_info_message +from capsulflask.db import my_exec_info_message bp = Blueprint("spoke", __name__, url_prefix="/spoke") @@ -20,6 +22,106 @@ def heartbeat(): # succeed or fail based on whether the request succeeds or fails. pass else: - current_app.logger.info(f"/hosts/heartbeat returned 401: invalid token") + current_app.logger.info(f"/hosts/heartbeat returned 401: invalid hub token") return abort(401, "invalid hub token") +@bp.route("/operation", methods=("POST")) +def operation(): + if authorized_as_hub(id): + request_body = request.json() + handlers = { + "capacity_avaliable": handle_capacity_avaliable, + "get": handle_get, + "list_ids": handle_list_ids, + "create": handle_create, + "destroy": handle_destroy, + } + + error_message = "" + types_csv = ", ".join(handlers.keys()) + if isinstance(request_body, dict) and 'type' in request_body: + if request_body['type'] in handlers: + return handlers[request_body['type']](request_body) + else: + error_message = f"'type' must be one of {types_csv}" + else: + error_message = "'type' json property is required" + + if error_message != "": + current_app.logger.info(f"/hosts/operation returned 400: {error_message}") + return abort(400, f"bad request; {error_message}") + else: + current_app.logger.info(f"/hosts/operation returned 401: invalid hub token") + return abort(401, "invalid hub token") + +def handle_capacity_avaliable(request_body): + if 'additional_ram_bytes' not in request_body: + current_app.logger.info(f"/hosts/operation returned 400: additional_ram_bytes is required for capacity_avaliable") + return abort(400, f"bad request; additional_ram_bytes is required for capacity_avaliable") + + has_capacity = current_app.config['SPOKE_MODEL'].capacity_avaliable(request_body['additional_ram_bytes']) + return jsonify(dict(assignment_status="assigned", capacity_avaliable=has_capacity)) + +def handle_get(request_body): + if 'id' not in request_body: + current_app.logger.info(f"/hosts/operation returned 400: id is required for get") + return abort(400, f"bad request; id is required for get") + + vm = current_app.config['SPOKE_MODEL'].get(request_body['id']) + + return jsonify(dict(assignment_status="assigned", id=vm.id, host=vm.host, ipv4=vm.ipv4, ipv6=vm.ipv6)) + +def handle_list_ids(request_body): + return jsonify(dict(assignment_status="assigned", ids=current_app.config['SPOKE_MODEL'].list_ids())) + +def handle_create(request_body): + parameters = ["operation_id", "email", "id", "template_image_file_name", "vcpus", "memory_mb", "ssh_public_keys"] + error_message = "" + for parameter in parameters: + if parameter not in request_body: + error_message = f"{error_message}\n{parameter} is required for create" + + if error_message != "": + current_app.logger.info(f"/hosts/operation returned 400: {error_message}") + return abort(400, f"bad request; {error_message}") + + # try to aquire operation_id + assignment_status = "assigned" + + if assignment_status == "assigned": + try: + current_app.config['SPOKE_MODEL'].create( + email=request_body['email'], + id=request_body['id'], + template_image_file_name=request_body['template_image_file_name'], + vcpus=request_body['vcpus'], + memory_mb=request_body['memory_mb'], + ssh_public_keys=request_body['ssh_public_keys'], + ) + except: + error_message = my_exec_info_message(sys.exc_info()) + params = f"email='{request_body['email']}', id='{request_body['id']}', " + params = f"{params}, template_image_file_name='{request_body['template_image_file_name']}', vcpus='{request_body['vcpus']}'" + params = f"{params}, memory_mb='{request_body['memory_mb']}', ssh_public_keys='{request_body['ssh_public_keys']}'" + current_app.logger.error(f"current_app.config['SPOKE_MODEL'].create({params}) failed: {error_message}") + return jsonify(dict(assignment_status=assignment_status, error_message=error_message)) + + return jsonify(dict(assignment_status=assignment_status)) + +def handle_destroy(request_body): + if 'id' not in request_body: + current_app.logger.info(f"/hosts/operation returned 400: id is required for destroy") + return abort(400, f"bad request; id is required for destroy") + + if 'email' not in request_body: + current_app.logger.info(f"/hosts/operation returned 400: email is required for destroy") + return abort(400, f"bad request; email is required for destroy") + + try: + current_app.config['SPOKE_MODEL'].destroy(id=request_body['id'], email=request_body['email']) + except: + error_message = my_exec_info_message(sys.exc_info()) + current_app.logger.error(f"current_app.config['SPOKE_MODEL'].destroy(id='{request_body['id']}', email='{request_body['email']}') failed: {error_message}") + return jsonify(dict(assignment_status="assigned", status="error", error_message=error_message)) + + return jsonify(dict(assignment_status="assigned", status="success")) \ No newline at end of file diff --git a/capsulflask/spoke_model.py b/capsulflask/spoke_model.py index 2cc6751..e01b390 100644 --- a/capsulflask/spoke_model.py +++ b/capsulflask/spoke_model.py @@ -156,12 +156,18 @@ class ShellScriptSpoke(SpokeInterface): {completedProcess.stderr} """) - def destroy(self, email: str, id: str) -> str: + def destroy(self, email: str, id: str): validate_capsul_id(id) completedProcess = run([join(current_app.root_path, 'shell_scripts/destroy.sh'), id], capture_output=True) self.validate_completed_process(completedProcess, email) lines = completedProcess.stdout.splitlines() status = lines[len(lines)-1].decode("utf-8") - return status + if not status == "success": + raise ValueError(f"""failed to destroy vm {id} for {email} on {current_app.config["SPOKE_HOST_ID"]}: + stdout: + {completedProcess.stdout} + stderr: + {completedProcess.stderr} + """)