trying to set up assignment of create operation
This commit is contained in:
parent
42a8e0c886
commit
d9c30e1ef8
@ -1,4 +1,6 @@
|
||||
|
||||
# I was never able to get this type hinting to work correctly
|
||||
# from psycopg2.extensions import connection as Psycopg2Connection, cursor as Psycopg2Cursor
|
||||
from nanoid import generate
|
||||
from flask import current_app
|
||||
from typing import List
|
||||
@ -10,6 +12,7 @@ class OnlineHost:
|
||||
self.url = url
|
||||
|
||||
class DBModel:
|
||||
#def __init__(self, connection: Psycopg2Connection, cursor: Psycopg2Cursor):
|
||||
def __init__(self, connection, cursor):
|
||||
self.connection = connection
|
||||
self.cursor = cursor
|
||||
@ -311,6 +314,30 @@ class DBModel:
|
||||
else:
|
||||
return None
|
||||
|
||||
def host_operation_exists(self, operation_id: int, host_id: str) -> bool:
|
||||
self.cursor.execute("SELECT operation FROM host_operation WHERE host = %s AND operation = %s",(host_id, operation_id))
|
||||
return len(self.cursor.fetchall()) != 0
|
||||
|
||||
def claim_operation(self, operation_id: int, host_id: str) -> bool:
|
||||
self.cursor.execute("""
|
||||
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
|
||||
BEGIN TRANSACTION;
|
||||
UPDATE host_operation SET assignment_status = 'assigned'
|
||||
WHERE host = %s AND operation = %s AND operation != (
|
||||
SELECT COALESCE(
|
||||
(SELECT operation FROM host_operation WHERE operation = %s AND assignment_status = 'assigned'),
|
||||
-1
|
||||
) as already_assigned_operation_id
|
||||
);
|
||||
COMMIT TRANSACTION;
|
||||
""", (host_id, operation_id, operation_id))
|
||||
|
||||
to_return = self.cursor.rowcount != 0
|
||||
|
||||
self.connection.commit()
|
||||
|
||||
return to_return
|
||||
|
||||
|
||||
|
||||
|
||||
|
@ -22,16 +22,20 @@ class MyHTTPClient:
|
||||
def make_requests_sync(self, online_hosts: List[OnlineHost], body: str) -> List(HTTPResult):
|
||||
self.event_loop.run_until_complete(self.make_requests(online_hosts=online_hosts, body=body))
|
||||
|
||||
def post_json_sync(self, method: str, url: str, body: str) -> HTTPResult:
|
||||
def post_json_sync(self, url: str, body: str, method="POST", authorization_header=None) -> HTTPResult:
|
||||
self.event_loop.run_until_complete(self.post_json_sync(method=method, url=url, body=body))
|
||||
|
||||
async def post_json(self, method: str, url: str, body: str) -> HTTPResult:
|
||||
async def post_json(self, url: str, body: str, method="POST", authorization_header=None) -> HTTPResult:
|
||||
response = None
|
||||
try:
|
||||
headers = {}
|
||||
if authorization_header != None:
|
||||
headers['Authorization'] = authorization_header
|
||||
response = await self.client_session.request(
|
||||
method=method,
|
||||
url=url,
|
||||
json=body,
|
||||
headers=headers,
|
||||
auth=aiohttp.BasicAuth("hub", current_app.config['HUB_TOKEN']),
|
||||
verify_ssl=True,
|
||||
)
|
||||
@ -60,7 +64,7 @@ class MyHTTPClient:
|
||||
# append to tasks in the same order as online_hosts
|
||||
for host in online_hosts:
|
||||
tasks.append(
|
||||
self.post_json(method="POST", url=host.url, body=body)
|
||||
self.post_json(url=host.url, body=body)
|
||||
)
|
||||
# gather is like Promise.all from javascript, it returns a future which resolves to an array of results
|
||||
# in the same order as the tasks that we passed in -- which were in the same order as online_hosts
|
||||
|
@ -12,11 +12,26 @@ def authorized_for_host(id):
|
||||
auth_header_value = request.headers.get('Authorization').replace("Bearer ", "")
|
||||
return get_model().authorized_for_host(id, auth_header_value)
|
||||
|
||||
@bp.route("/heartbeat/<string:id>", methods=("POST"))
|
||||
def heartbeat(id):
|
||||
if authorized_for_host(id):
|
||||
get_model().host_heartbeat(id)
|
||||
@bp.route("/heartbeat/<string:host_id>", methods=("POST"))
|
||||
def heartbeat(host_id):
|
||||
if authorized_for_host(host_id):
|
||||
get_model().host_heartbeat(host_id)
|
||||
else:
|
||||
current_app.logger.info(f"/hub/heartbeat/{id} returned 401: invalid token")
|
||||
current_app.logger.info(f"/hub/heartbeat/{host_id} returned 401: invalid token")
|
||||
return abort(401, "invalid host id or token")
|
||||
|
||||
@bp.route("/claim-operation/<int:operation_id>/<string:host_id>", methods=("POST"))
|
||||
def claim_operation(operation_id: int, host_id: str):
|
||||
if authorized_for_host(host_id):
|
||||
exists = get_model().host_operation_exists(operation_id, host_id)
|
||||
if not exists:
|
||||
return abort(404, "host operation not found")
|
||||
claimed = get_model().claim_operation(operation_id, host_id)
|
||||
if claimed:
|
||||
return "ok"
|
||||
else:
|
||||
return abort(409, "operation was already assigned to another host")
|
||||
else:
|
||||
current_app.logger.info(f"/hub/claim-operation/{operation_id}/{host_id} returned 401: invalid token")
|
||||
return abort(401, "invalid host id or token")
|
||||
|
||||
|
@ -64,7 +64,8 @@ class CapsulFlaskHub(VirtualizationInterface):
|
||||
|
||||
def generic_operation(self, hosts: List[OnlineHost], payload: str, immediate_mode: bool) -> Tuple[int, List[HTTPResult]]:
|
||||
operation_id = get_model().create_operation(hosts, payload)
|
||||
results = current_app.config["HTTP_CLIENT"].make_requests_sync(hosts, payload)
|
||||
authorization_header = f"Bearer {current_app.config['HUB_TOKEN']}"
|
||||
results = current_app.config["HTTP_CLIENT"].make_requests_sync(hosts, payload, authorization_header=authorization_header)
|
||||
for i in range(len(hosts)):
|
||||
host = hosts[i]
|
||||
result = results[i]
|
||||
|
@ -29,5 +29,4 @@ CREATE TABLE host_operation (
|
||||
PRIMARY KEY (host, operation)
|
||||
);
|
||||
|
||||
|
||||
UPDATE schemaversion SET version = 9;
|
||||
UPDATE schemaversion SET version = 9;
|
||||
|
@ -18,9 +18,20 @@ def authorized_as_hub(id):
|
||||
@bp.route("/heartbeat", methods=("POST"))
|
||||
def heartbeat():
|
||||
if authorized_as_hub(id):
|
||||
# make request to hub-domain.com/hub/heartbeat/{current_app.config["SPOKE_HOST_ID"]}
|
||||
# succeed or fail based on whether the request succeeds or fails.
|
||||
pass
|
||||
url = f"{current_app.config['HUB_URL']}/hub/heartbeat/{current_app.config['SPOKE_HOST_ID']}"
|
||||
authorization_header = f"Bearer {current_app.config['SPOKE_HOST_TOKEN']}"
|
||||
result = current_app.config['HTTP_CLIENT'].post_json_sync(url, body=None, authorization_header=authorization_header)
|
||||
if result.status_code == -1:
|
||||
current_app.logger.info(f"/hosts/heartbeat returned 503: hub at {url} timed out or cannot be reached")
|
||||
return abort(503, "Service Unavailable: hub timed out or cannot be reached")
|
||||
if result.status_code == 401:
|
||||
current_app.logger.info(f"/hosts/heartbeat returned 502: hub at {url} rejected our token")
|
||||
return abort(502, "hub rejected our token")
|
||||
if result.status_code != 200:
|
||||
current_app.logger.info(f"/hosts/heartbeat returned 502: hub at {url} returned {result.status_code}")
|
||||
return abort(502, "Bad Gateway: hub did not return 200")
|
||||
|
||||
return "OK"
|
||||
else:
|
||||
current_app.logger.info(f"/hosts/heartbeat returned 401: invalid hub token")
|
||||
return abort(401, "invalid hub token")
|
||||
@ -90,9 +101,21 @@ def handle_create(request_body):
|
||||
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"
|
||||
# only one host should create the vm, so we first race to assign this create operation to ourselves.
|
||||
# only one host will win this race
|
||||
authorization_header = f"Bearer {current_app.config['SPOKE_HOST_TOKEN']}"
|
||||
url = f"{current_app.config['HUB_URL']}/hub/claim-operation/{request_body['operation_id']}/{current_app.config['SPOKE_HOST_ID']}"
|
||||
result = current_app.config['HTTP_CLIENT'].post_json_sync(url, body=None, authorization_header=authorization_header)
|
||||
|
||||
assignment_status = ""
|
||||
if result.status_code == 200:
|
||||
assignment_status = "assigned"
|
||||
elif result.status_code == 409:
|
||||
assignment_status = "assigned_to_other_host"
|
||||
else:
|
||||
current_app.logger.info(f"{url} returned {result.status_code}: {result.body}")
|
||||
return abort(503, f"hub did not cleanly handle our request to claim the create operation")
|
||||
|
||||
if assignment_status == "assigned":
|
||||
try:
|
||||
current_app.config['SPOKE_MODEL'].create(
|
||||
@ -108,7 +131,7 @@ def handle_create(request_body):
|
||||
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}")
|
||||
current_app.logger.error(f"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))
|
||||
|
Loading…
Reference in New Issue
Block a user