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
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 "<missing>"
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 "<missing>"
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
)

View File

@ -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

View File

@ -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:

View File

@ -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"
echo "$exists $state $ipv4"

View File

@ -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()))

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":"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)

View File

@ -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;
}

View File

@ -4,15 +4,34 @@
{% block content %}
{% if delete %}
{% if deleted %}
{% if deleted %}
<div class="row third-margin">
<h1>DELETED</h1>
</div>
<div class="row third-margin">
<p>{{ vm['id'] }} has been deleted.</p>
</div>
{% else %}
{% if force_stop %}
<div class="row third-margin">
<h1>DELETED</h1>
<h1>Are you sure?</h1>
</div>
<div class="row third-margin">
<p>{{ vm['id'] }} has been deleted.</p>
<p>
Are you sure you want to force stop {{ vm['id'] }}?
This would be like unplugging the power supply.
</p>
</div>
{% else %}
<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">
<h1>Are you sure?</h1>
</div>
@ -21,147 +40,173 @@
</div>
<div class="row third-margin">
<a href="/console/{{ vm['id'] }}">No, Cancel!</a>
<form id="delete_action" method="post">
<input type="hidden" name="delete" value="True"/>
<form method="post">
<input type="hidden" name="action" value="delete"/>
<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, Delete">
</form>
</div>
{% endif %}
{% else %}
<div class="row third-margin">
<h1>{{ vm['id'] }}</h1>
</div>
<div class="row wrap grid-large third-margin">
{% else %}
<div class="row third-margin">
<h1>{{ vm['id'] }}</h1>
</div>
<div class="row wrap grid-large third-margin">
<div class="row justify-start">
<label class="align" for="created">Created</label>
<span id="created">{{ vm['created'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="size">Capsul Size</label>
<span id="size">{{ vm['size'] }}</span>
</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">
<label class="align" for="dollars_per_month">Monthly Cost</label>
<span id="dollars_per_month">${{ vm['dollars_per_month'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="ipv4">IPv4 Address</label>
<span id="ipv4">{{ vm['ipv4'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="os_description">Operating System</label>
<span id="os_description">{{ vm['os_description'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="vcpus">VCPUs</label>
<span id="vcpus">{{ vm['vcpus'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="memory_mb">Memory</label>
<span id="memory_mb">{{ vm['memory_mb'] }}MB</span>
</div>
<div class="row justify-start">
<label class="align" for="bandwidth_gb_per_month">Bandwidth</label>
<span id="bandwidth_gb_per_month">{{ vm['bandwidth_gb_per_month'] }}GB/month</span>
</div>
<div class="row justify-start">
<label class="align" for="ssh_username">SSH Username</label>
<span id="ssh_username">cyberian</span>
</div>
<div class="row justify-start">
<label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label>
<a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a>
</div>
<div class="row justify-start">
<label class="align" for="created">Created</label>
<span id="created">{{ vm['created'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="size">Capsul Size</label>
<span id="size">{{ vm['size'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="dollars_per_month">Monthly Cost</label>
<span id="dollars_per_month">${{ vm['dollars_per_month'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="ipv4">IPv4 Address</label>
<span id="ipv4">{{ vm['ipv4'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="os_description">Operating System</label>
<span id="os_description">{{ vm['os_description'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="vcpus">VCPUs</label>
<span id="vcpus">{{ vm['vcpus'] }}</span>
</div>
<div class="row justify-start">
<label class="align" for="memory_mb">Memory</label>
<span id="memory_mb">{{ vm['memory_mb'] }}MB</span>
</div>
<div class="row justify-start">
<label class="align" for="bandwidth_gb_per_month">Bandwidth</label>
<span id="bandwidth_gb_per_month">{{ vm['bandwidth_gb_per_month'] }}GB/month</span>
</div>
<div class="row justify-start">
<label class="align" for="ssh_username">SSH Username</label>
<span id="ssh_username">cyberian</span>
</div>
<div class="row justify-start">
<label class="align" for="ssh_authorized_keys">SSH Authorized Keys</label>
<a id="ssh_authorized_keys" href="/console/ssh">{{ vm['ssh_authorized_keys'] }}</a>
</div>
<div class="row center justify-start">
<div class="row center justify-start vm-actions">
<label class="align" for="delete_action">Actions</label>
<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="submit" class="form-submit-link" value="Delete...">
</form>
</div>
</div>
<div class="row third-margin">
<h1>ssh host key fingerprints</h1>
</div>
<div class="row">
<pre class="code">{% for key in vm['ssh_host_keys'] %}
SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}</pre>
</div>
<div class="row">
<span>(What's this? see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>)</span>
</div>
<div class="row ">
<hr/>
</div>
<div class="row third-margin">
{% for d in durations %}
<a href="/console/{{ vm['id'] }}?duration={{ d }}">
{% if d == duration %}
<span class="code">{{ d }}</span>
{% else %}
{{ d }}
{% endif %}
{% 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 %}
</a>
{% endfor %}
</div>
<div class="row wrap grid-small justify-end">
<div class="metric">
<h1>cpu</h1>
<a href="/metrics/html/cpu/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/cpu/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>memory</h1>
<a href="/metrics/html/memory/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/memory/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
<div class="row third-margin">
<h1>ssh host key fingerprints</h1>
</div>
<div class="metric">
<h1>network_in</h1>
<a href="/metrics/html/network_in/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/network_in/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
<div class="row">
<pre class="code">{% for key in vm['ssh_host_keys'] %}
SHA256:{{ key.sha256 }} ({{ key.key_type }}){% endfor %}</pre>
</div>
<div class="row">
<span>(What's this? see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>)</span>
</div>
<div class="row ">
<hr/>
</div>
<div class="row third-margin">
{% for d in durations %}
<a href="/console/{{ vm['id'] }}?duration={{ d }}">
{% if d == duration %}
<span class="code">{{ d }}</span>
{% else %}
{{ d }}
{% endif %}
<div class="metric">
<h1>network_out</h1>
<a href="/metrics/html/network_out/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/network_out/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</a>
{% endfor %}
</div>
<div class="row wrap grid-small justify-end">
<div class="metric">
<h1>cpu</h1>
<a href="/metrics/html/cpu/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/cpu/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>disk</h1>
<a href="/metrics/html/disk/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/disk/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
<div class="metric">
<h1>memory</h1>
<a href="/metrics/html/memory/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/memory/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>network_in</h1>
<a href="/metrics/html/network_in/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/network_in/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>network_out</h1>
<a href="/metrics/html/network_out/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/network_out/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
<div class="metric">
<h1>disk</h1>
<a href="/metrics/html/disk/{{ vm['id'] }}/{{ duration }}">
<img src="/metrics/disk/{{ vm['id'] }}/{{ duration }}/m"/>
</a>
</div>
</div>
</div>
<div class="row ">
<hr/>
</div>
<div class="row half-margin">
add the following to your ~/.ssh/known_hosts file (optional)
</div>
<div class="row">
<pre class="code wrap break-all smalltext">{% for key in vm['ssh_host_keys'] %}
{{ vm['ipv4'] }} {{ key.content }}{% endfor %}
</pre>
</div>
<div class="row">
<span>(What's this? see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>)</span>
</div>
<div class="row ">
<hr/>
</div>
<div class="row half-margin">
add the following to your ~/.ssh/known_hosts file (optional)
</div>
<div class="row">
<pre class="code wrap break-all smalltext">{% for key in vm['ssh_host_keys'] %}
{{ vm['ipv4'] }} {{ key.content }}{% endfor %}
</pre>
</div>
<div class="row">
<span>(What's this? see <a href="/about-ssh">Understanding the Secure Shell Protocol (SSH)</a>)</span>
</div>
{% endif %}
{% endif %}
{% endblock %}
{% block pagesource %}/templates/create-capsul.html{% endblock %}

View File

@ -21,6 +21,7 @@
<table>
<thead>
<tr>
<th class="heart-icon"></th>
<th>id</th>
<th>size</th>
<th>cpu</th>
@ -33,6 +34,14 @@
<tbody>
{% for vm in vms %}
<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>{{ vm["size"] }}</td>
<td class="metrics"><img src="/metrics/cpu/{{ vm['id'] }}/5m/s"/></td>