From e8348052a815f0685e710127f96ab5612892a707 Mon Sep 17 00:00:00 2001 From: forest Date: Tue, 16 Feb 2021 21:13:51 -0600 Subject: [PATCH] fixing get inconsistency and adding vm_state_command --- capsulflask/console.py | 4 ++-- capsulflask/hub_model.py | 25 +++++++++++++++++++++++++ capsulflask/shared.py | 3 +++ capsulflask/shell_scripts/get.sh | 12 ++++++++---- capsulflask/spoke_api.py | 30 +++++++++++++++++++++++++----- capsulflask/spoke_model.py | 27 ++++++++++++++++++++++++--- 6 files changed, 87 insertions(+), 14 deletions(-) diff --git a/capsulflask/console.py b/capsulflask/console.py index 9029649..a9722b3 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -30,11 +30,11 @@ def makeCapsulId(): def double_check_capsul_address(id, ipv4, get_ssh_host_keys): try: result = current_app.config["HUB_MODEL"].get(id, get_ssh_host_keys) - if result != None and result.ipv4 != ipv4: + if result != None and result.ipv4 != None and result.ipv4 != ipv4: ipv4 = result.ipv4 get_model().update_vm_ip(email=session["account"], id=id, ipv4=result.ipv4) - if result != None and get_ssh_host_keys: + if result != None and result.ipv4 != None and get_ssh_host_keys: get_model().update_vm_ssh_host_keys(email=session["account"], id=id, ssh_host_keys=result.ssh_host_keys) except: current_app.logger.error(f""" diff --git a/capsulflask/hub_model.py b/capsulflask/hub_model.py index bf30abb..fb3510e 100644 --- a/capsulflask/hub_model.py +++ b/capsulflask/hub_model.py @@ -45,6 +45,9 @@ class MockHub(VirtualizationInterface): def destroy(self, email: str, id: str): current_app.logger.info(f"mock destroy: {id} for {email}") + def vm_state_command(self, email: str, id: str, command: str): + current_app.logger.info(f"mock {command}: {id} for {email}") + class CapsulFlaskHub(VirtualizationInterface): @@ -236,3 +239,25 @@ class CapsulFlaskHub(VirtualizationInterface): if not result_status == "success": raise ValueError(f"""failed to destroy vm "{id}" on host "{host.id}" for {email}: {result_json_string}""") + + + def vm_state_command(self, email: str, id: str, command: str): + validate_capsul_id(id) + result_status = None + host = get_model().host_of_capsul(id) + if host is not None: + payload = json.dumps(dict(type="vm_state_command", email=email, id=id, command=command)) + results = self.synchronous_operation([host], payload) + result_json_string = "" + for result in results: + try: + result_json_string = result.body + result_body = json.loads(result_json_string) + if isinstance(result_body, dict) and 'status' in result_body: + result_status = result_body['status'] + except: + pass + + if not result_status == "success": + raise ValueError(f"""failed to {command} vm "{id}" on host "{host.id}" for {email}: {result_json_string}""") + diff --git a/capsulflask/shared.py b/capsulflask/shared.py index 00c6805..68f2282 100644 --- a/capsulflask/shared.py +++ b/capsulflask/shared.py @@ -39,6 +39,9 @@ class VirtualizationInterface: def destroy(self, email: str, id: str): pass + def vm_state_command(self, email: str, id: str, command: str): + pass + def validate_capsul_id(id): if not re.match(r"^(cvm|capsul)-[a-z0-9]{10}$", id): raise ValueError(f"vm id \"{id}\" must match \"^capsul-[a-z0-9]{{10}}$\"") diff --git a/capsulflask/shell_scripts/get.sh b/capsulflask/shell_scripts/get.sh index cf5da92..7aae056 100755 --- a/capsulflask/shell_scripts/get.sh +++ b/capsulflask/shell_scripts/get.sh @@ -1,3 +1,4 @@ + #!/bin/sh vmname="$1" @@ -7,10 +8,13 @@ if echo "$vmname" | grep -vqE '^(cvm|capsul)-[a-z0-9]{10}$'; then exit 1 fi -if ! virsh list --name --all | grep -qE "^$vmname$" ; then - echo "Error: $vmname not found" - exit 1 +# this will let us know if the vm exists or not +exists="false" +if virsh domuuid "$vmname" | grep -vqE '^[\t\s\n]*$'; then + exists="true" fi # this gets the ipv4 -virsh domifaddr "$vmname" | awk '/vnet/ {print $4}' | cut -d'/' -f1 +ipv4="$(virsh domifaddr "$vmname" | awk '/vnet/ {print $4}' | cut -d'/' -f1)" + +echo "$exists $ipv4" \ No newline at end of file diff --git a/capsulflask/spoke_api.py b/capsulflask/spoke_api.py index 3f6dd75..bb0486d 100644 --- a/capsulflask/spoke_api.py +++ b/capsulflask/spoke_api.py @@ -52,6 +52,7 @@ def operation_impl(operation_id: int): "list_ids": handle_list_ids, "create": handle_create, "destroy": handle_destroy, + "vm_state_command": handle_vm_state_command, } error_message = "" @@ -90,11 +91,8 @@ def handle_get(operation_id, request_body): return abort(400, f"bad request; id is required for get") vm = current_app.config['SPOKE_MODEL'].get(request_body['id'], request_body['get_ssh_host_keys']) - # TODO vm can be None when the capsul exists on this host but has no IP address yet - # when this happens it logs an "error reading assignment_status" - # To fix this we need to - # 1. modify shell script to return does it exist on this host + if it does whats its ip - # 2. if exists but no ip, return assigned with no ip + if vm is None: + return jsonify(dict(assignment_status="assigned")) return jsonify(dict(assignment_status="assigned", id=vm.id, host=vm.host, ipv4=vm.ipv4, ipv6=vm.ipv6, ssh_host_keys=vm.ssh_host_keys)) @@ -167,4 +165,26 @@ def handle_destroy(operation_id, request_body): 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")) + + +def handle_vm_state_command(operation_id, request_body): + + required_properties = ['id', 'email', 'command'] + for required_property in required_properties: + if required_property not in request_body: + current_app.logger.error(f"/hosts/operation returned 400: {required_property} is required for vm_state_command") + return abort(400, f"bad request; {required_property} is required for vm_state_command") + + if request_body['command'] not in ["stop", "force-stop", "start", "restart"]: + current_app.logger.error(f"/hosts/operation returned 400: command ({request_body['command']}) must be one of stop, force-stop, start, or restart") + return abort(400, f"bad request; command ({request_body['command']}) must be one of stop, force-stop, start, or restart") + + try: + current_app.config['SPOKE_MODEL'].vm_state_command(id=request_body['id'], email=request_body['email'], command=request_body['command']) + except: + error_message = my_exec_info_message(sys.exc_info()) + current_app.logger.error(f"current_app.config['SPOKE_MODEL'].vm_state_command(id='{request_body['id']}', email='{request_body['email']}, command='{request_body['command']}') 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 ae437c0..0bc5c0a 100644 --- a/capsulflask/spoke_model.py +++ b/capsulflask/spoke_model.py @@ -41,6 +41,9 @@ class MockSpoke(VirtualizationInterface): def destroy(self, email: str, id: str): current_app.logger.info(f"mock destroy: {id} for {email}") + def vm_state_command(self, email: str, id: str, command: str): + current_app.logger.info(f"mock {command}: {id} for {email}") + class ShellScriptSpoke(VirtualizationInterface): def validate_completed_process(self, completedProcess, email=None): @@ -85,11 +88,20 @@ class ShellScriptSpoke(VirtualizationInterface): lines = completedProcess.stdout.splitlines() if len(lines) == 0: return None - - ipaddr = lines[0].decode("utf-8") + + result_string = lines[0].decode("utf-8") + + fields = result_string.split(" ") + if fields[0] != "true": + return None + + if len(fields) < 2: + return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"]) + + ipaddr = fields[1] if not re.match(r"^([0-9]{1,3}\.){3}[0-9]{1,3}$", ipaddr): - return None + return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"]) if get_ssh_host_keys: try: @@ -172,4 +184,13 @@ class ShellScriptSpoke(VirtualizationInterface): {completedProcess.stderr} """) + def vm_state_command(self, email: str, id: str, command: str): + validate_capsul_id(id) + if command not in ["stop", "force-stop", "start", "restart"]: + raise ValueError(f"command ({command}) must be one of stop, force-stop, start, or restart") + + completedProcess = run([join(current_app.root_path, f"shell_scripts/{command}.sh"), id], capture_output=True) + self.validate_completed_process(completedProcess, email) + returned_string = completedProcess.stdout.decode("utf-8") + current_app.logger.info(f"{command} vm {id} for {email} returned: {returned_string}")