first try at implementing the vm start and stop feature

This commit is contained in:
forest 2021-02-17 20:50:17 -06:00
parent e8348052a8
commit ba0b29462c
9 changed files with 296 additions and 159 deletions

View File

@ -34,7 +34,7 @@ def double_check_capsul_address(id, ipv4, get_ssh_host_keys):
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 result.ipv4 != None and get_ssh_host_keys: if result != None and result.ssh_host_keys != 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"""
@ -62,11 +62,13 @@ def index():
result = double_check_capsul_address(vm["id"], vm["ipv4"], False) result = double_check_capsul_address(vm["id"], vm["ipv4"], False)
if result is not None: if result is not None:
vm["ipv4"] = result.ipv4 vm["ipv4"] = result.ipv4
vm["state"] = result.state
vms = list(map( vms = list(map(
lambda x: dict( lambda x: dict(
id=x['id'], id=x['id'],
size=x['size'], size=x['size'],
state=x['state'],
ipv4=(x['ipv4'] if x['ipv4'] else "..booting.."), ipv4=(x['ipv4'] if x['ipv4'] else "..booting.."),
ipv4_status=("ok" if x['ipv4'] else "waiting-pulse"), ipv4_status=("ok" if x['ipv4'] else "waiting-pulse"),
os=x['os'], os=x['os'],
@ -92,24 +94,58 @@ def detail(id):
if vm['deleted']: if vm['deleted']:
return render_template("capsul-detail.html", vm=vm, delete=True, deleted=True) return render_template("capsul-detail.html", vm=vm, delete=True, deleted=True)
vm["created"] = vm['created'].strftime("%b %d %Y %H:%M")
vm["ssh_authorized_keys"] = ", ".join(vm["ssh_authorized_keys"]) if len(vm["ssh_authorized_keys"]) > 0 else "<missing>"
if request.method == "POST": if request.method == "POST":
if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']:
return abort(418, f"u want tea") return abort(418, f"u want tea")
if 'action' not in request.form:
return abort(400, "action is required")
if request.form['action'] == "start":
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="start")
vm["state"] = "starting"
return render_template("capsul-detail.html", vm=vm)
elif request.form['action'] == "delete":
if 'are_you_sure' not in request.form or not request.form['are_you_sure']: if 'are_you_sure' not in request.form or not request.form['are_you_sure']:
return render_template( return render_template(
"capsul-detail.html", "capsul-detail.html",
csrf_token = session["csrf-token"], csrf_token = session["csrf-token"],
vm=vm, vm=vm,
delete=True, delete=True
deleted=False
) )
else: else:
current_app.logger.info(f"deleting {vm['id']} per user request ({session['account']})") current_app.logger.info(f"deleting {vm['id']} per user request ({session['account']})")
current_app.config["HUB_MODEL"].destroy(email=session['account'], id=id) current_app.config["HUB_MODEL"].destroy(email=session['account'], id=id)
get_model().delete_vm(email=session['account'], id=id) get_model().delete_vm(email=session['account'], id=id)
return render_template("capsul-detail.html", vm=vm, delete=True, deleted=True) return render_template("capsul-detail.html", vm=vm, deleted=True)
elif request.form['action'] == "force-stop":
if 'are_you_sure' not in request.form or not request.form['are_you_sure']:
return render_template(
"capsul-detail.html",
csrf_token = session["csrf-token"],
vm=vm,
force_stop=True,
)
else:
current_app.logger.info(f"force stopping {vm['id']} per user request ({session['account']})")
current_app.config["HUB_MODEL"].vm_state_command(email=session['account'], id=id, command="force-stop")
vm["state"] = "stopped"
return render_template(
"capsul-detail.html",
csrf_token = session["csrf-token"],
vm=vm,
durations=list(map(lambda x: x.strip("_"), metric_durations.keys())),
duration=duration
)
else:
return abort(400, "action must be either delete, force-stop, or start")
else: else:
needs_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0 needs_ssh_host_keys = "ssh_host_keys" not in vm or len(vm["ssh_host_keys"]) == 0
@ -118,17 +154,17 @@ def detail(id):
if vm_from_virt_model is not None: if vm_from_virt_model is not None:
vm["ipv4"] = vm_from_virt_model.ipv4 vm["ipv4"] = vm_from_virt_model.ipv4
vm["state"] = vm_from_virt_model.state
if needs_ssh_host_keys: if needs_ssh_host_keys:
vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys
vm["created"] = vm['created'].strftime("%b %d %Y %H:%M") if vm["state"] == "running" and not vm["ipv4"]:
vm["ssh_authorized_keys"] = ", ".join(vm["ssh_authorized_keys"]) if len(vm["ssh_authorized_keys"]) > 0 else "<missing>" vm["state"] = "starting"
return render_template( return render_template(
"capsul-detail.html", "capsul-detail.html",
csrf_token = session["csrf-token"], csrf_token = session["csrf-token"],
vm=vm, vm=vm,
delete=False,
durations=list(map(lambda x: x.strip("_"), metric_durations.keys())), durations=list(map(lambda x: x.strip("_"), metric_durations.keys())),
duration=duration duration=duration
) )

View File

@ -144,8 +144,8 @@ class CapsulFlaskHub(VirtualizationInterface):
for result in results: for result in results:
try: try:
result_body = json.loads(result.body) result_body = json.loads(result.body)
if isinstance(result_body, dict) and ('ipv4' in result_body or 'ipv6' in result_body): if isinstance(result_body, dict) and ('state' in result_body):
return VirtualMachine(id, host=host, ipv4=result_body['ipv4'], ipv6=result_body['ipv6'], ssh_host_keys=result_body['ssh_host_keys']) return VirtualMachine(id, host=host, state=result_body['state'], ipv4=result_body['ipv4'], ipv6=result_body['ipv6'], ssh_host_keys=result_body['ssh_host_keys'])
except: except:
pass pass

View File

@ -16,11 +16,12 @@ class OnlineHost:
# self.sha256 = sha256 # self.sha256 = sha256
class VirtualMachine: class VirtualMachine:
def __init__(self, id, host, ipv4=None, ipv6=None, ssh_host_keys: List[dict] = list()): def __init__(self, id, host, ipv4=None, ipv6=None, state="unknown", ssh_host_keys: List[dict] = list()):
self.id = id self.id = id
self.host = host self.host = host
self.ipv4 = ipv4 self.ipv4 = ipv4
self.ipv6 = ipv6 self.ipv6 = ipv6
self.state = state
self.ssh_host_keys = ssh_host_keys self.ssh_host_keys = ssh_host_keys
class VirtualizationInterface: class VirtualizationInterface:

View File

@ -10,11 +10,27 @@ fi
# this will let us know if the vm exists or not # this will let us know if the vm exists or not
exists="false" exists="false"
if virsh domuuid "$vmname" | grep -vqE '^[\t\s\n]*$'; then state = "unknown"
if ! virsh domuuid "$vmname" | grep -qE '^[\t\s\n]*$'; then
exists="true" exists="true"
state_code="$(virsh domstats $vmname | grep state.state | cut -d '=' -f 2)"
if ! printf "$state_code" | grep -qE '^[0-8]$'; then
printf 'state_code was not detected. state_code %s must match ^[0-8]$\n' "$state_code"
exit 1
fi
case "$state_code" in
1) state = "running" ;;
2) state = "blocked" ;;
4) state = "stopping" ;;
6) state = "crashed" ;;
[357]) state = "stopped" ;;
esac
fi fi
# this gets the ipv4 # this gets the ipv4
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" echo "$exists $state $ipv4"

View File

@ -94,7 +94,7 @@ def handle_get(operation_id, request_body):
if vm is None: if vm is None:
return jsonify(dict(assignment_status="assigned")) 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)) return jsonify(dict(assignment_status="assigned", id=vm.id, host=vm.host, state=vm.state, ipv4=vm.ipv4, ipv6=vm.ipv6, ssh_host_keys=vm.ssh_host_keys))
def handle_list_ids(operation_id, request_body): def handle_list_ids(operation_id, request_body):
return jsonify(dict(assignment_status="assigned", ids=current_app.config['SPOKE_MODEL'].list_ids())) return jsonify(dict(assignment_status="assigned", ids=current_app.config['SPOKE_MODEL'].list_ids()))

View File

@ -26,9 +26,9 @@ class MockSpoke(VirtualizationInterface):
{"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"}, {"key_type":"RSA", "content":"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvotgzgEP65JUQ8S8OoNKy1uEEPEAcFetSp7QpONe6hj4wPgyFNgVtdoWdNcU19dX3hpdse0G8OlaMUTnNVuRlbIZXuifXQ2jTtCFUA2mmJ5bF+XjGm3TXKMNGh9PN+wEPUeWd14vZL+QPUMev5LmA8cawPiU5+vVMLid93HRBj118aCJFQxLgrdP48VPfKHFRfCR6TIjg1ii3dH4acdJAvlmJ3GFB6ICT42EmBqskz2MPe0rIFxH8YohCBbAbrbWYcptHt4e48h4UdpZdYOhEdv89GrT8BF2C5cbQ5i9qVpI57bXKrj8hPZU5of48UHLSpXG8mbH0YDiOQOfKX/Mt", "sha256":"ghee6KzRnBJhND2kEUZSaouk7CD6o6z2aAc8GPkV+GQ"},
{"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"} {"key_type":"ECDSA", "content":"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLLgOoATz9R4aS2kk7vWoxX+lshK63t9+5BIHdzZeFE1o+shlcf0Wji8cN/L1+m3bi0uSETZDOAWMP3rHLJj9Hk=", "sha256":"aCYG1aD8cv/TjzJL0bi9jdabMGksdkfa7R8dCGm1yYs"}
]""") ]""")
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1", ssh_host_keys=ssh_host_keys) return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1", state="running", ssh_host_keys=ssh_host_keys)
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1") return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4="1.1.1.1", state="running")
def list_ids(self) -> list: def list_ids(self) -> list:
return get_model().all_non_deleted_vm_ids() return get_model().all_non_deleted_vm_ids()
@ -98,24 +98,29 @@ class ShellScriptSpoke(VirtualizationInterface):
if len(fields) < 2: if len(fields) < 2:
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"]) return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"])
ipaddr = fields[1] state = fields[1]
if len(fields) < 3:
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state)
ipaddr = fields[2]
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 VirtualMachine(id, current_app.config["SPOKE_HOST_ID"]) return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state)
if get_ssh_host_keys: if get_ssh_host_keys:
try: try:
completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True) completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True)
self.validate_completed_process(completedProcess2) self.validate_completed_process(completedProcess2)
ssh_host_keys = json.loads(completedProcess2.stdout.decode("utf-8")) ssh_host_keys = json.loads(completedProcess2.stdout.decode("utf-8"))
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipaddr, ssh_host_keys=ssh_host_keys) return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr, ssh_host_keys=ssh_host_keys)
except: except:
current_app.logger.warning(f""" current_app.logger.warning(f"""
failed to ssh-keyscan {id} at {ipaddr}: failed to ssh-keyscan {id} at {ipaddr}:
{my_exec_info_message(sys.exc_info())}""" {my_exec_info_message(sys.exc_info())}"""
) )
return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], ipv4=ipaddr) return VirtualMachine(id, current_app.config["SPOKE_HOST_ID"], state=state, ipv4=ipaddr)
def list_ids(self) -> list: def list_ids(self) -> list:
completedProcess = run([join(current_app.root_path, 'shell_scripts/list-ids.sh')], capture_output=True) completedProcess = run([join(current_app.root_path, 'shell_scripts/list-ids.sh')], capture_output=True)

View File

@ -131,7 +131,8 @@ pre.wrap {
white-space: normal; white-space: normal;
} }
label.align { label.align,
.vm-actions form {
min-width: 10em; min-width: 10em;
} }
@ -250,11 +251,26 @@ table.small td, table.small th {
table.small td.metrics { table.small td.metrics {
padding: 0; padding: 0;
} }
th.heart-icon {
font-size: calc(0.40rem + 2.3vmin);
line-height: 1rem;
padding-left: 0.3rem;
padding-right: 0.2rem;
}
td.capsul-status {
line-height: 1rem;
padding: 0;
padding-left: 0.07em;
font-size: calc(0.4rem + 3.4vmin);
padding-top: 0.2rem;
}
td.metrics img { td.metrics img {
margin-left: -20px; margin-left: -1.2em;
margin-right: -20px; margin-right: -1.2em;
margin-top: -5px; margin-top: -0.25em;
margin-bottom: -5px; margin-bottom: -0.25em;
width: 4.3em;
} }
th { th {
border-right: 4px solid #241e1e; border-right: 4px solid #241e1e;
@ -301,6 +317,15 @@ pre.code.wrap {
color: #777e73bb; color: #777e73bb;
} }
.red {
color: #c21d00;
}
.green {
color: #069e5f;
}
footer, p { footer, p {
text-align: left; text-align: left;
} }

View File

@ -4,7 +4,6 @@
{% block content %} {% block content %}
{% if delete %}
{% if deleted %} {% if deleted %}
<div class="row third-margin"> <div class="row third-margin">
<h1>DELETED</h1> <h1>DELETED</h1>
@ -13,6 +12,26 @@
<p>{{ vm['id'] }} has been deleted.</p> <p>{{ vm['id'] }} has been deleted.</p>
</div> </div>
{% else %} {% else %}
{% if force_stop %}
<div class="row third-margin">
<h1>Are you sure?</h1>
</div>
<div class="row third-margin">
<p>
Are you sure you want to force stop {{ vm['id'] }}?
This would be like unplugging the power supply.
</p>
</div>
<div class="row third-margin">
<a href="/console/{{ vm['id'] }}">No, Cancel!</a>
<form method="post">
<input type="hidden" name="action" value="force-stop"/>
<input type="hidden" name="are_you_sure" value="True"/>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<input type="submit" class="form-submit-link" value="Yes, Force Stop">
</form>
</div>
{% elif delete %}
<div class="row third-margin"> <div class="row third-margin">
<h1>Are you sure?</h1> <h1>Are you sure?</h1>
</div> </div>
@ -21,15 +40,13 @@
</div> </div>
<div class="row third-margin"> <div class="row third-margin">
<a href="/console/{{ vm['id'] }}">No, Cancel!</a> <a href="/console/{{ vm['id'] }}">No, Cancel!</a>
<form id="delete_action" method="post"> <form method="post">
<input type="hidden" name="delete" value="True"/> <input type="hidden" name="action" value="delete"/>
<input type="hidden" name="are_you_sure" value="True"/> <input type="hidden" name="are_you_sure" value="True"/>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<input type="submit" class="form-submit-link" value="Yes, Delete"> <input type="submit" class="form-submit-link" value="Yes, Delete">
</form> </form>
</div> </div>
{% endif %}
{% else %} {% else %}
<div class="row third-margin"> <div class="row third-margin">
<h1>{{ vm['id'] }}</h1> <h1>{{ vm['id'] }}</h1>
@ -44,6 +61,16 @@
<label class="align" for="size">Capsul Size</label> <label class="align" for="size">Capsul Size</label>
<span id="size">{{ vm['size'] }}</span> <span id="size">{{ vm['size'] }}</span>
</div> </div>
<div class="row justify-start">
<label class="align" for="vm_state">State</label>
{% if vm['state'] == 'starting' or vm['state'] == 'stopping' %}
<span id="vm_state" class="waiting-pulse">{{ vm['state'] }}</span>
{% elif vm['state'] == 'crashed' or vm['state'] == 'blocked' %}
<span id="vm_state" class="red">{{ vm['state'] }}</span>
{% else %}
<span id="vm_state">{{ vm['state'] }}</span>
{% endif %}
</div>
<div class="row justify-start"> <div class="row justify-start">
<label class="align" for="dollars_per_month">Monthly Cost</label> <label class="align" for="dollars_per_month">Monthly Cost</label>
<span id="dollars_per_month">${{ vm['dollars_per_month'] }}</span> <span id="dollars_per_month">${{ vm['dollars_per_month'] }}</span>
@ -76,14 +103,30 @@
<label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label> <label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label>
<a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a> <a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a>
</div> </div>
<div class="row center justify-start">
</div>
<div class="row center justify-start vm-actions">
<label class="align" for="delete_action">Actions</label> <label class="align" for="delete_action">Actions</label>
<form id="delete_action" method="post"> <form id="delete_action" method="post">
<input type="hidden" name="delete" value="True"/> <input type="hidden" name="action" value="delete"/>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/> <input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<input type="submit" class="form-submit-link" value="Delete..."> <input type="submit" class="form-submit-link" value="Delete...">
</form> </form>
</div> {% if vm['state'] == 'crashed' or vm['state'] == 'stopped' %}
<form id="start_action" method="post">
<input type="hidden" name="action" value="start"/>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<input type="submit" class="form-submit-link" value="Start">
</form>
{% endif %}
{% if vm['state'] != 'stopped' %}
<form id="force_stop_action" method="post">
<input type="hidden" name="action" value="force-stop"/>
<input type="hidden" name="csrf-token" value="{{ csrf_token }}"/>
<input type="submit" class="form-submit-link" value="Force Stop...">
</form>
{% endif %}
</div> </div>
<div class="row third-margin"> <div class="row third-margin">
<h1>ssh host key fingerprints</h1> <h1>ssh host key fingerprints</h1>
@ -162,6 +205,8 @@ SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}</pre>
<span>(What's this? see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>)</span> <span>(What's this? see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>)</span>
</div> </div>
{% endif %} {% endif %}
{% endif %}
{% endblock %} {% endblock %}
{% block pagesource %}/templates/create-capsul.html{% endblock %} {% block pagesource %}/templates/create-capsul.html{% endblock %}

View File

@ -21,6 +21,7 @@
<table> <table>
<thead> <thead>
<tr> <tr>
<th class="heart-icon"></th>
<th>id</th> <th>id</th>
<th>size</th> <th>size</th>
<th>cpu</th> <th>cpu</th>
@ -33,6 +34,14 @@
<tbody> <tbody>
{% for vm in vms %} {% for vm in vms %}
<tr> <tr>
{% if vm['state'] == 'starting' or vm['state'] == 'stopping' %}
<td class="capsul-status waiting-pulse"></td>
{% elif vm['state'] == 'crashed' or vm['state'] == 'blocked' or vm['state'] == 'stopped' %}
<td class="capsul-status red"></td>
{% else %}
<td class="capsul-status green"></td>
{% endif %}
<td><a class="no-shadow" href="/console/{{ vm['id'] }}">{{ vm["id"] }}</a></td> <td><a class="no-shadow" href="/console/{{ vm['id'] }}">{{ vm["id"] }}</a></td>
<td>{{ vm["size"] }}</td> <td>{{ vm["size"] }}</td>
<td class="metrics"><img src="/metrics/cpu/{{ vm['id'] }}/5m/s"/></td> <td class="metrics"><img src="/metrics/cpu/{{ vm['id'] }}/5m/s"/></td>