fixing get inconsistency and adding vm_state_command

This commit is contained in:
forest 2021-02-16 21:13:51 -06:00
parent 2e265703bd
commit e8348052a8
6 changed files with 87 additions and 14 deletions

View File

@ -30,11 +30,11 @@ def makeCapsulId():
def double_check_capsul_address(id, ipv4, get_ssh_host_keys): def double_check_capsul_address(id, ipv4, get_ssh_host_keys):
try: try:
result = current_app.config["HUB_MODEL"].get(id, get_ssh_host_keys) 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 ipv4 = result.ipv4
get_model().update_vm_ip(email=session["account"], id=id, 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) get_model().update_vm_ssh_host_keys(email=session["account"], id=id, ssh_host_keys=result.ssh_host_keys)
except: except:
current_app.logger.error(f""" current_app.logger.error(f"""

View File

@ -45,6 +45,9 @@ class MockHub(VirtualizationInterface):
def destroy(self, email: str, id: str): def destroy(self, email: str, id: str):
current_app.logger.info(f"mock destroy: {id} for {email}") 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): class CapsulFlaskHub(VirtualizationInterface):
@ -236,3 +239,25 @@ class CapsulFlaskHub(VirtualizationInterface):
if not result_status == "success": if not result_status == "success":
raise ValueError(f"""failed to destroy vm "{id}" on host "{host.id}" for {email}: {result_json_string}""") 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 = "<no response from host>"
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}""")

View File

@ -39,6 +39,9 @@ class VirtualizationInterface:
def destroy(self, email: str, id: str): def destroy(self, email: str, id: str):
pass pass
def vm_state_command(self, email: str, id: str, command: str):
pass
def validate_capsul_id(id): def validate_capsul_id(id):
if not re.match(r"^(cvm|capsul)-[a-z0-9]{10}$", 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}}$\"") raise ValueError(f"vm id \"{id}\" must match \"^capsul-[a-z0-9]{{10}}$\"")

View File

@ -1,3 +1,4 @@
#!/bin/sh #!/bin/sh
vmname="$1" vmname="$1"
@ -7,10 +8,13 @@ if echo "$vmname" | grep -vqE '^(cvm|capsul)-[a-z0-9]{10}$'; then
exit 1 exit 1
fi fi
if ! virsh list --name --all | grep -qE "^$vmname$" ; then # this will let us know if the vm exists or not
echo "Error: $vmname not found" exists="false"
exit 1 if virsh domuuid "$vmname" | grep -vqE '^[\t\s\n]*$'; then
exists="true"
fi fi
# this gets the ipv4 # 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"

View File

@ -52,6 +52,7 @@ def operation_impl(operation_id: int):
"list_ids": handle_list_ids, "list_ids": handle_list_ids,
"create": handle_create, "create": handle_create,
"destroy": handle_destroy, "destroy": handle_destroy,
"vm_state_command": handle_vm_state_command,
} }
error_message = "" error_message = ""
@ -90,11 +91,8 @@ def handle_get(operation_id, request_body):
return abort(400, f"bad request; 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'], request_body['get_ssh_host_keys']) 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 if vm is None:
# when this happens it logs an "error reading assignment_status" return jsonify(dict(assignment_status="assigned"))
# 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
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)) 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}") 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="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")) return jsonify(dict(assignment_status="assigned", status="success"))

View File

@ -41,6 +41,9 @@ class MockSpoke(VirtualizationInterface):
def destroy(self, email: str, id: str): def destroy(self, email: str, id: str):
current_app.logger.info(f"mock destroy: {id} for {email}") 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): class ShellScriptSpoke(VirtualizationInterface):
def validate_completed_process(self, completedProcess, email=None): def validate_completed_process(self, completedProcess, email=None):
@ -85,11 +88,20 @@ class ShellScriptSpoke(VirtualizationInterface):
lines = completedProcess.stdout.splitlines() lines = completedProcess.stdout.splitlines()
if len(lines) == 0: if len(lines) == 0:
return None 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): 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: if get_ssh_host_keys:
try: try:
@ -172,4 +184,13 @@ class ShellScriptSpoke(VirtualizationInterface):
{completedProcess.stderr} {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}")