diff --git a/capsulflask/console.py b/capsulflask/console.py index a9722b3..b0d4c41 100644 --- a/capsulflask/console.py +++ b/capsulflask/console.py @@ -34,7 +34,7 @@ def double_check_capsul_address(id, ipv4, get_ssh_host_keys): 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) except: current_app.logger.error(f""" @@ -62,11 +62,13 @@ def index(): result = double_check_capsul_address(vm["id"], vm["ipv4"], False) if result is not None: vm["ipv4"] = result.ipv4 + vm["state"] = result.state vms = list(map( lambda x: dict( id=x['id'], size=x['size'], + state=x['state'], ipv4=(x['ipv4'] if x['ipv4'] else "..booting.."), ipv4_status=("ok" if x['ipv4'] else "waiting-pulse"), os=x['os'], @@ -92,24 +94,58 @@ def detail(id): if vm['deleted']: 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 "" + if request.method == "POST": if "csrf-token" not in request.form or request.form['csrf-token'] != session['csrf-token']: return abort(418, f"u want tea") - 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, - delete=True, - deleted=False - ) - else: - current_app.logger.info(f"deleting {vm['id']} per user request ({session['account']})") - current_app.config["HUB_MODEL"].destroy(email=session['account'], id=id) - get_model().delete_vm(email=session['account'], id=id) + 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']: + return render_template( + "capsul-detail.html", + csrf_token = session["csrf-token"], + vm=vm, + delete=True + ) + else: + current_app.logger.info(f"deleting {vm['id']} per user request ({session['account']})") + current_app.config["HUB_MODEL"].destroy(email=session['account'], id=id) + get_model().delete_vm(email=session['account'], id=id) + + 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") - return render_template("capsul-detail.html", vm=vm, delete=True, deleted=True) else: 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: vm["ipv4"] = vm_from_virt_model.ipv4 + vm["state"] = vm_from_virt_model.state if needs_ssh_host_keys: vm["ssh_host_keys"] = vm_from_virt_model.ssh_host_keys - - 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 "" + + if vm["state"] == "running" and not vm["ipv4"]: + vm["state"] = "starting" return render_template( "capsul-detail.html", csrf_token = session["csrf-token"], vm=vm, - delete=False, durations=list(map(lambda x: x.strip("_"), metric_durations.keys())), duration=duration ) diff --git a/capsulflask/hub_model.py b/capsulflask/hub_model.py index fb3510e..840cefb 100644 --- a/capsulflask/hub_model.py +++ b/capsulflask/hub_model.py @@ -144,8 +144,8 @@ class CapsulFlaskHub(VirtualizationInterface): for result in results: try: result_body = json.loads(result.body) - if isinstance(result_body, dict) and ('ipv4' in result_body or 'ipv6' in result_body): - return VirtualMachine(id, host=host, ipv4=result_body['ipv4'], ipv6=result_body['ipv6'], ssh_host_keys=result_body['ssh_host_keys']) + if isinstance(result_body, dict) and ('state' in result_body): + 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: pass diff --git a/capsulflask/shared.py b/capsulflask/shared.py index 68f2282..ba72900 100644 --- a/capsulflask/shared.py +++ b/capsulflask/shared.py @@ -16,11 +16,12 @@ class OnlineHost: # self.sha256 = sha256 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.host = host self.ipv4 = ipv4 self.ipv6 = ipv6 + self.state = state self.ssh_host_keys = ssh_host_keys class VirtualizationInterface: diff --git a/capsulflask/shell_scripts/get.sh b/capsulflask/shell_scripts/get.sh index 7aae056..f88284d 100755 --- a/capsulflask/shell_scripts/get.sh +++ b/capsulflask/shell_scripts/get.sh @@ -10,11 +10,27 @@ fi # this will let us know if the vm exists or not 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" + + 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 # this gets the ipv4 ipv4="$(virsh domifaddr "$vmname" | awk '/vnet/ {print $4}' | cut -d'/' -f1)" -echo "$exists $ipv4" \ No newline at end of file +echo "$exists $state $ipv4" \ No newline at end of file diff --git a/capsulflask/spoke_api.py b/capsulflask/spoke_api.py index bb0486d..198af95 100644 --- a/capsulflask/spoke_api.py +++ b/capsulflask/spoke_api.py @@ -94,7 +94,7 @@ def handle_get(operation_id, request_body): 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)) + 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): return jsonify(dict(assignment_status="assigned", ids=current_app.config['SPOKE_MODEL'].list_ids())) diff --git a/capsulflask/spoke_model.py b/capsulflask/spoke_model.py index 0bc5c0a..069e390 100644 --- a/capsulflask/spoke_model.py +++ b/capsulflask/spoke_model.py @@ -26,9 +26,9 @@ class MockSpoke(VirtualizationInterface): {"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"} ]""") - 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: return get_model().all_non_deleted_vm_ids() @@ -98,24 +98,29 @@ class ShellScriptSpoke(VirtualizationInterface): if len(fields) < 2: 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): - 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: try: completedProcess2 = run([join(current_app.root_path, 'shell_scripts/ssh-keyscan.sh'), ipaddr], capture_output=True) self.validate_completed_process(completedProcess2) 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: current_app.logger.warning(f""" failed to ssh-keyscan {id} at {ipaddr}: {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: completedProcess = run([join(current_app.root_path, 'shell_scripts/list-ids.sh')], capture_output=True) diff --git a/capsulflask/static/style.css b/capsulflask/static/style.css index 0d01de0..b1f4554 100644 --- a/capsulflask/static/style.css +++ b/capsulflask/static/style.css @@ -131,7 +131,8 @@ pre.wrap { white-space: normal; } -label.align { +label.align, +.vm-actions form { min-width: 10em; } @@ -250,11 +251,26 @@ table.small td, table.small th { table.small td.metrics { 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 { - margin-left: -20px; - margin-right: -20px; - margin-top: -5px; - margin-bottom: -5px; + margin-left: -1.2em; + margin-right: -1.2em; + margin-top: -0.25em; + margin-bottom: -0.25em; + width: 4.3em; } th { border-right: 4px solid #241e1e; @@ -301,6 +317,15 @@ pre.code.wrap { color: #777e73bb; } +.red { + color: #c21d00; +} + +.green { + color: #069e5f; +} + + footer, p { text-align: left; } diff --git a/capsulflask/templates/capsul-detail.html b/capsulflask/templates/capsul-detail.html index 91a87b6..ba10521 100644 --- a/capsulflask/templates/capsul-detail.html +++ b/capsulflask/templates/capsul-detail.html @@ -4,15 +4,34 @@ {% block content %} -{% if delete %} - {% if deleted %} +{% if deleted %} +
+

DELETED

+
+
+

{{ vm['id'] }} has been deleted.

+
+{% else %} + {% if force_stop %}
-

DELETED

+

Are you sure?

-

{{ vm['id'] }} has been deleted.

+

+ Are you sure you want to force stop {{ vm['id'] }}? + This would be like unplugging the power supply. +

- {% else %} +
+ No, Cancel! +
+ + + + +
+
+ {% elif delete %}

Are you sure?

@@ -21,147 +40,173 @@
No, Cancel! -
- + +
- - {% endif %} -{% else %} -
-

{{ vm['id'] }}

-
-
+ {% else %} +
+

{{ vm['id'] }}

+
+
+ +
+ + {{ vm['created'] }} +
+
+ + {{ vm['size'] }} +
+
+ + {% if vm['state'] == 'starting' or vm['state'] == 'stopping' %} + {{ vm['state'] }} + {% elif vm['state'] == 'crashed' or vm['state'] == 'blocked' %} + {{ vm['state'] }} + {% else %} + {{ vm['state'] }} + {% endif %} +
+
+ + ${{ vm['dollars_per_month'] }} +
+
+ + {{ vm['ipv4'] }} +
+
+ + {{ vm['os_description'] }} +
+
+ + {{ vm['vcpus'] }} +
+
+ + {{ vm['memory_mb'] }}MB +
+
+ + {{ vm['bandwidth_gb_per_month'] }}GB/month +
+
+ + cyberian +
+
+ + {{ vm['ssh_authorized_keys'] }} +
-
- - {{ vm['created'] }}
-
- - {{ vm['size'] }} -
-
- - ${{ vm['dollars_per_month'] }} -
-
- - {{ vm['ipv4'] }} -
-
- - {{ vm['os_description'] }} -
-
- - {{ vm['vcpus'] }} -
-
- - {{ vm['memory_mb'] }}MB -
-
- - {{ vm['bandwidth_gb_per_month'] }}GB/month -
-
- - cyberian -
-
- - {{ vm['ssh_authorized_keys'] }} -
-
+
- +
-
-
-
-

ssh host key fingerprints

-
- -
-
{% for key in vm['ssh_host_keys'] %}
-SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}
-
- -
-
-
- -
-
-

cpu

- - -
- -
-

memory

- - - +
+

ssh host key fingerprints

-
-

network_in

- - - +
+
{% for key in vm['ssh_host_keys'] %}
+  SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}
+ +
+
+
+
+ {% for d in durations %} + + {% if d == duration %} + {{ d }} + {% else %} + {{ d }} + {% endif %} -
-

network_out

-
- - + + {% endfor %}
+
+
+

cpu

+ + + +
-
-

disk

- - - +
+

memory

+ + + +
+ +
+

network_in

+ + + +
+ +
+

network_out

+ + + +
+ +
+

disk

+ + + +
-
-
-
-
-
- add the following to your ~/.ssh/known_hosts file (optional) -
-
-
{% for key in vm['ssh_host_keys'] %}
-{{ vm['ipv4'] }} {{ key.content }}{% endfor %}
-
-
- +
+
+
+
+ add the following to your ~/.ssh/known_hosts file (optional) +
+
+
{% for key in vm['ssh_host_keys'] %}
+  {{ vm['ipv4'] }} {{ key.content }}{% endfor %}
+  
+
+ + {% endif %} {% endif %} + {% endblock %} {% block pagesource %}/templates/create-capsul.html{% endblock %} diff --git a/capsulflask/templates/capsuls.html b/capsulflask/templates/capsuls.html index 0122089..ea8dfc6 100644 --- a/capsulflask/templates/capsuls.html +++ b/capsulflask/templates/capsuls.html @@ -21,6 +21,7 @@ + @@ -33,6 +34,14 @@ {% for vm in vms %} + {% if vm['state'] == 'starting' or vm['state'] == 'stopping' %} + + {% elif vm['state'] == 'crashed' or vm['state'] == 'blocked' or vm['state'] == 'stopped' %} + + {% else %} + + {% endif %} +
id size cpu
{{ vm["id"] }} {{ vm["size"] }}