summaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorJack Morgan <jack.morgan@intel.com>2016-08-29 18:25:32 +0000
committerGerrit Code Review <gerrit@172.30.200.206>2016-08-29 18:25:32 +0000
commite70af736b5c9152f5c8c3b1a44b3a1ddf286cb2c (patch)
tree6be74477e31e9ad106d83974a49d5e14ebdb3bb0 /tools
parentc3338109091fe880d7dd40966b9e7aa2ad7fd132 (diff)
parentb67557fdf3ddab95d2834c8aa01dcc0d120685dd (diff)
Merge "Add a Resource detail view"
Diffstat (limited to 'tools')
-rw-r--r--tools/pharos-dashboard/dashboard/admin.py3
-rw-r--r--tools/pharos-dashboard/dashboard/models.py19
-rw-r--r--tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py7
-rw-r--r--tools/pharos-dashboard/dashboard/templatetags/jira_filters.py8
-rw-r--r--tools/pharos-dashboard/dashboard/urls.py4
-rw-r--r--tools/pharos-dashboard/dashboard/views.py30
-rw-r--r--tools/pharos-dashboard/jenkins/models.py13
-rw-r--r--tools/pharos-dashboard/templates/booking/booking_table.html33
-rw-r--r--tools/pharos-dashboard/templates/dashboard/dev_pods.html2
-rw-r--r--tools/pharos-dashboard/templates/dashboard/lab_owner.html151
-rw-r--r--tools/pharos-dashboard/templates/dashboard/resource.html58
-rw-r--r--tools/pharos-dashboard/templates/dashboard/resource_all.html (renamed from tools/pharos-dashboard/templates/dashboard/resource_utilization.html)66
-rw-r--r--tools/pharos-dashboard/templates/dashboard/resource_detail.html64
-rw-r--r--tools/pharos-dashboard/templates/dashboard/server_table.html30
14 files changed, 275 insertions, 213 deletions
diff --git a/tools/pharos-dashboard/dashboard/admin.py b/tools/pharos-dashboard/dashboard/admin.py
index 990e63e8..71a1e7d2 100644
--- a/tools/pharos-dashboard/dashboard/admin.py
+++ b/tools/pharos-dashboard/dashboard/admin.py
@@ -1,5 +1,6 @@
from django.contrib import admin
-from dashboard.models import Resource
+from dashboard.models import *
admin.site.register(Resource)
+admin.site.register(Server)
diff --git a/tools/pharos-dashboard/dashboard/models.py b/tools/pharos-dashboard/dashboard/models.py
index 971af6a2..d645cd55 100644
--- a/tools/pharos-dashboard/dashboard/models.py
+++ b/tools/pharos-dashboard/dashboard/models.py
@@ -1,6 +1,5 @@
from django.contrib.auth.models import User
from django.db import models
-from django.utils import timezone
from jenkins.models import JenkinsSlave
@@ -17,4 +16,20 @@ class Resource(models.Model):
db_table = 'resource'
def __str__(self):
- return self.name \ No newline at end of file
+ return self.name
+
+
+class Server(models.Model):
+ id = models.AutoField(primary_key=True)
+ resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
+ name = models.CharField(max_length=100, blank=True)
+ model = models.CharField(max_length=100, blank=True)
+ cpu = models.CharField(max_length=100, blank=True)
+ ram = models.CharField(max_length=100, blank=True)
+ storage = models.CharField(max_length=100, blank=True)
+
+ class Meta:
+ db_table = 'server'
+
+ def __str__(self):
+ return self.name
diff --git a/tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py b/tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py
index f7e00a87..f5038ea5 100644
--- a/tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py
+++ b/tools/pharos-dashboard/dashboard/templatetags/jenkins_filters.py
@@ -9,7 +9,7 @@ def jenkins_job_color(job_result):
return '#d9534f'
if job_result == 'UNSTABLE':
return '#EDD62B'
- return '#646F73' # job is still building
+ return '#646F73' # job is still building
@register.filter
@@ -21,7 +21,8 @@ def jenkins_status_color(slave_status):
if slave_status == 'online / idle':
return '#5bc0de'
+
@register.filter
def jenkins_job_blink(job_result):
- if job_result == '': # job is still building
- return 'class=blink_me' \ No newline at end of file
+ if job_result == '': # job is still building
+ return 'class=blink_me'
diff --git a/tools/pharos-dashboard/dashboard/templatetags/jira_filters.py b/tools/pharos-dashboard/dashboard/templatetags/jira_filters.py
new file mode 100644
index 00000000..d9c27612
--- /dev/null
+++ b/tools/pharos-dashboard/dashboard/templatetags/jira_filters.py
@@ -0,0 +1,8 @@
+from django.template.defaultfilters import register
+
+from pharos_dashboard import settings
+
+
+@register.filter
+def jira_issue_url(issue):
+ return settings.JIRA_URL + '/browse/' + str(issue)
diff --git a/tools/pharos-dashboard/dashboard/urls.py b/tools/pharos-dashboard/dashboard/urls.py
index 51d764c4..809204c1 100644
--- a/tools/pharos-dashboard/dashboard/urls.py
+++ b/tools/pharos-dashboard/dashboard/urls.py
@@ -21,8 +21,8 @@ urlpatterns = [
url(r'^ci_pods/$', CIPodsView.as_view(), name='ci_pods'),
url(r'^dev_pods/$', DevelopmentPodsView.as_view(), name='dev_pods'),
url(r'^jenkins_slaves/$', JenkinsSlavesView.as_view(), name='jenkins_slaves'),
- url(r'^resource/all/', LabOwnerView.as_view(),
- name='resources'),
+ url(r'^resource/all/$', LabOwnerView.as_view(), name='resources'),
+ url(r'^resource/(?P<resource_id>[0-9]+)/$', ResourceView.as_view(), name='resource'),
url(r'^$', DevelopmentPodsView.as_view(), name="index"),
]
diff --git a/tools/pharos-dashboard/dashboard/views.py b/tools/pharos-dashboard/dashboard/views.py
index 56b3a510..ef1845c8 100644
--- a/tools/pharos-dashboard/dashboard/views.py
+++ b/tools/pharos-dashboard/dashboard/views.py
@@ -1,12 +1,12 @@
from datetime import timedelta
-from django.contrib.auth.models import User
+from django.shortcuts import get_object_or_404
from django.utils import timezone
from django.views.generic import TemplateView
from booking.models import Booking
from dashboard.models import Resource
-from jenkins.models import JenkinsSlave, JenkinsStatistic
+from jenkins.models import JenkinsSlave
class JenkinsSlavesView(TemplateView):
@@ -51,25 +51,27 @@ class DevelopmentPodsView(TemplateView):
return context
+class ResourceView(TemplateView):
+ template_name = "dashboard/resource.html"
+
+ def get_context_data(self, **kwargs):
+ resource = get_object_or_404(Resource, id=self.kwargs['resource_id'])
+ utilization = resource.slave.get_utilization(timedelta(days=7))
+ bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now())
+ context = super(ResourceView, self).get_context_data(**kwargs)
+ context.update({'title': str(resource), 'resource': resource, 'utilization': utilization, 'bookings': bookings})
+ return context
+
+
class LabOwnerView(TemplateView):
- template_name = "dashboard/lab_owner.html"
+ template_name = "dashboard/resource_all.html"
def get_context_data(self, **kwargs):
resources = Resource.objects.filter(slave__dev_pod=True)
pods = []
for resource in resources:
- utilization = {'idle': 0, 'online': 0, 'offline': 0}
- # query measurement points for the last week
- statistics = JenkinsStatistic.objects.filter(slave=resource.slave,
- timestamp__gte=timezone.now() - timedelta(
- days=7))
-
- utilization['idle'] = statistics.filter(idle=True).count()
- utilization['online'] = statistics.filter(online=True).count()
- utilization['offline'] = statistics.filter(offline=True).count()
-
+ utilization = resource.slave.get_utilization(timedelta(days=7))
bookings = Booking.objects.filter(resource=resource, end__gt=timezone.now())
-
pods.append((resource, utilization, bookings))
context = super(LabOwnerView, self).get_context_data(**kwargs)
context.update({'title': "Overview", 'pods': pods})
diff --git a/tools/pharos-dashboard/jenkins/models.py b/tools/pharos-dashboard/jenkins/models.py
index 438a8827..354887ab 100644
--- a/tools/pharos-dashboard/jenkins/models.py
+++ b/tools/pharos-dashboard/jenkins/models.py
@@ -1,4 +1,5 @@
from django.db import models
+from django.utils import timezone
class JenkinsSlave(models.Model):
@@ -18,6 +19,18 @@ class JenkinsSlave(models.Model):
last_job_installer = models.CharField(max_length=50, default='')
last_job_result = models.CharField(max_length=30, default='')
+ def get_utilization(self, timedelta):
+ """
+ Return a dictionary containing the count of idle, online and offline measurements in the time from
+ now-timedelta to now
+ """
+ utilization = {'idle': 0, 'online': 0, 'offline': 0}
+ statistics = self.jenkinsstatistic_set.filter(timestamp__gte=timezone.now() - timedelta)
+ utilization['idle'] = statistics.filter(idle=True).count()
+ utilization['online'] = statistics.filter(online=True).count()
+ utilization['offline'] = statistics.filter(offline=True).count()
+ return utilization
+
class Meta:
db_table = 'jenkins_slave'
diff --git a/tools/pharos-dashboard/templates/booking/booking_table.html b/tools/pharos-dashboard/templates/booking/booking_table.html
new file mode 100644
index 00000000..3d0b7575
--- /dev/null
+++ b/tools/pharos-dashboard/templates/booking/booking_table.html
@@ -0,0 +1,33 @@
+{% load jira_filters %}
+
+
+<thead>
+<tr>
+ <th>User</th>
+ <th>Purpose</th>
+ <th>Start</th>
+ <th>End</th>
+ <th>Jira</th>
+</tr>
+</thead>
+<tbody>
+{% for booking in bookings %}
+ <tr>
+ <th>
+ {{ booking.user.username }}
+ </th>
+ <th>
+ {{ booking.purpose }}
+ </th>
+ <th>
+ {{ booking.start }}
+ </th>
+ <th>
+ {{ booking.end }}
+ </th>
+ <th><a target='_blank'
+ href={{ booking.get_jira_issue | jira_issue_url }}>{{ booking.get_jira_issue }}</a>
+ </th>
+ </tr>
+{% endfor %}
+</tbody> \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/dev_pods.html b/tools/pharos-dashboard/templates/dashboard/dev_pods.html
index 532a3a11..9c84bb91 100644
--- a/tools/pharos-dashboard/templates/dashboard/dev_pods.html
+++ b/tools/pharos-dashboard/templates/dashboard/dev_pods.html
@@ -18,7 +18,7 @@
{% for pod, booking in dev_pods %}
<tr>
<th>
- <a target='_blank' href={{ pod.url }}>{{ pod.name }}</a>
+ <a href={% url 'dashboard:resource' resource_id=pod.id %}>{{ pod.name }}</a>
</th>
<th>
<a target='_blank' href={{ pod.slave.url }}>{{ pod.slave.name }}</a>
diff --git a/tools/pharos-dashboard/templates/dashboard/lab_owner.html b/tools/pharos-dashboard/templates/dashboard/lab_owner.html
deleted file mode 100644
index a4f428c7..00000000
--- a/tools/pharos-dashboard/templates/dashboard/lab_owner.html
+++ /dev/null
@@ -1,151 +0,0 @@
-{% extends "base.html" %}
-{% load staticfiles %}
-
-{% block extrahead %}
- <!-- Morris Charts CSS -->
- <link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
-
- <!-- DataTables CSS -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
- rel="stylesheet">
-
- <!-- DataTables Responsive CSS -->
- <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
- rel="stylesheet">
-{% endblock extrahead %}
-
-
-{% block content %}
- {% for resource, utilization, bookings in pods %}
- <div class="row">
- <div class="col-lg-3">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{ resource.name }}
- </div>
- <div class="panel-body">
- <div class="flot-chart">
- <div class="flot-chart-content" id="{{ resource.slave.name }}"></div>
- </div>
- </div>
- </div>
- </div>
- <div class="col-lg-6">
- <div class="panel panel-default">
- <div class="panel-heading">
- {{ resource.name }} Bookings
- </div>
- <div class="panel-body">
- <div class="dataTables_wrapper">
- <table class="table table-striped table-bordered table-hover"
- id="{{ resource.slave.name }}_bookings" cellspacing="0"
- width="100%">
- <thead>
- <tr>
- <th>User</th>
- <th>Purpose</th>
- <th>Start</th>
- <th>End</th>
- <th>Status</th>
- </tr>
- </thead>
- <tbody>
- {% for booking in bookings %}
- <tr>
- <th>
- {{ booking.user.username }}
- </th>
- <th>
- {{ booking.purpose }}
- </th>
- <th>
- {{ booking.start }}
- </th>
- <th>
- {{ booking.end }}
- </th>
- <th>
- Jira Status
- </th>
- </tr>
- {% endfor %}`
- </tbody>
- </table>
- </div>
- </div>
- </div>
- </div>
- </div>
- {% endfor %}
-
-{% endblock content %}
-
-
-{% block extrajs %}
- <!-- DataTables JavaScript -->
- <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
- rel="stylesheet">
-
-
- <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
- <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
-
-
-
- <!-- Flot Charts JavaScript -->
- <script src="{% static "bower_components/flot/excanvas.min.js" %}"></script>
- <script src="{% static "bower_components/flot/jquery.flot.js" %}"></script>
- <script src="{% static "bower_components/flot/jquery.flot.pie.js" %}"></script>
- <script src="{% static "bower_components/flot/jquery.flot.resize.js" %}"></script>
- <script src="{% static "bower_components/flot/jquery.flot.time.js" %}"></script>
- <script src="{% static "bower_components/flot.tooltip/js/jquery.flot.tooltip.min.js" %}"></script>
-
- <script type="text/javascript">
- $(document).ready(function () {
-
-
- {% for resource, utilization, bookings in pods %}
- $('#{{ resource.slave.name }}_bookings').DataTable({});
-
- $(function () {
- var data = [{
- label: "Offline",
- data: {{ utilization.offline }},
- color: '#d9534f'
- }, {
- label: "Online",
- data: {{ utilization.online }},
- color: '#5cb85c'
- }, {
- label: "Idle",
- data: {{ utilization.idle }},
- color: '#5bc0de'
- }];
-
- var plotObj = $.plot($("#{{ resource.slave.name }}"), data, {
- series: {
- pie: {
- show: true
- }
- },
- grid: {
- hoverable: false
- },
- tooltip: true,
- tooltipOpts: {
- content: "%p.0%, %s", // show percentages, rounding to 2 decimal places
- shifts: {
- x: 20,
- y: 0
- },
- defaultTheme: false
- }
- });
-
- });
- {% endfor %}
-
- });
- </script>
-
-{% endblock extrajs %} \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/resource.html b/tools/pharos-dashboard/templates/dashboard/resource.html
new file mode 100644
index 00000000..92d02f66
--- /dev/null
+++ b/tools/pharos-dashboard/templates/dashboard/resource.html
@@ -0,0 +1,58 @@
+{% extends "base.html" %}
+{% load staticfiles %}
+
+{% block extrahead %}
+ <!-- Morris Charts CSS -->
+ <link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
+
+ <!-- DataTables CSS -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <!-- DataTables Responsive CSS -->
+ <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ rel="stylesheet">
+{% endblock extrahead %}
+
+
+{% block content %}
+ {% include "dashboard/resource_detail.html" %}
+{% endblock content %}
+
+
+{% block extrajs %}
+ <!-- DataTables JavaScript -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+
+
+
+ <!-- Flot Charts JavaScript -->
+ <script src="{% static "bower_components/flot/excanvas.min.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.pie.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.resize.js" %}"></script>
+ <script src="{% static "bower_components/flot/jquery.flot.time.js" %}"></script>
+ <script src="{% static "bower_components/flot.tooltip/js/jquery.flot.tooltip.min.js" %}"></script>
+
+ <script type="text/javascript">
+ $(document).ready(function () {
+ $('#{{ resource.id }}_server_table').DataTable({});
+ $('#{{ resource.id }}_bookings_table').DataTable({});
+
+ $(function () {
+ var plotObj = $.plot($("#{{ resource.id }}_slave_utilization"), data_{{ resource.id }}, {
+ series: {
+ pie: {
+ show: true
+ }
+ }
+ });
+
+ });
+ });
+ </script>
+{% endblock extrajs %} \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/resource_utilization.html b/tools/pharos-dashboard/templates/dashboard/resource_all.html
index fb483d60..2078475f 100644
--- a/tools/pharos-dashboard/templates/dashboard/resource_utilization.html
+++ b/tools/pharos-dashboard/templates/dashboard/resource_all.html
@@ -5,31 +5,43 @@
<!-- Morris Charts CSS -->
<link href="{% static "bower_components/morrisjs/morris.css" %}" rel="stylesheet">
+ <!-- DataTables CSS -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <!-- DataTables Responsive CSS -->
+ <link href="{% static "bower_components/datatables-responsive/css/dataTables.responsive.css" %}"
+ rel="stylesheet">
{% endblock extrahead %}
{% block content %}
- <div class="row">
- {% for resource, utilization in pods %}
- <div class="col-lg-3">
+ {% for resource, utilization, bookings in pods %}
+ <div class="row">
+ <div class="col-lg-12">
<div class="panel panel-default">
<div class="panel-heading">
{{ resource.name }}
</div>
<div class="panel-body">
- <div class="flot-chart">
- <div class="flot-chart-content" id="{{ resource.slave.name }}"></div>
- </div>
+ {% include "dashboard/resource_detail.html" %}
</div>
</div>
</div>
- {% endfor %}
- </div>
-
+ </div>
+ {% endfor %}
{% endblock content %}
{% block extrajs %}
+ <!-- DataTables JavaScript -->
+ <link href="{% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.css" %}"
+ rel="stylesheet">
+
+ <script src={% static "bower_components/datatables/media/js/jquery.dataTables.min.js" %}></script>
+ <script src={% static "bower_components/datatables-plugins/integration/bootstrap/3/dataTables.bootstrap.min.js" %}></script>
+
+
<!-- Flot Charts JavaScript -->
<script src="{% static "bower_components/flot/excanvas.min.js" %}"></script>
@@ -41,46 +53,22 @@
<script type="text/javascript">
$(document).ready(function () {
- {% for resource, utilization in pods %}
- $(function () {
- var data = [{
- label: "Offline",
- data: {{ utilization.offline }},
- color: '#d9534f'
- }, {
- label: "Online",
- data: {{ utilization.online }},
- color: '#5cb85c'
- }, {
- label: "Idle",
- data: {{ utilization.idle }},
- color: '#5bc0de'
- }];
+ {% for resource, utilization, bookings in pods %}
+
+ $('#{{ resource.id }}_server_table').DataTable({});
+ $('#{{ resource.id }}_bookings_table').DataTable({});
- var plotObj = $.plot($("#{{ resource.slave.name }}"), data, {
+ $(function () {
+ var plotObj = $.plot($("#{{ resource.id }}_slave_utilization"), data_{{ resource.id }}, {
series: {
pie: {
show: true
}
- },
- grid: {
- hoverable: true
- },
- tooltip: true,
- tooltipOpts: {
- content: "%p.0%, %s", // show percentages, rounding to 2 decimal places
- shifts: {
- x: 20,
- y: 0
- },
- defaultTheme: false
}
});
});
{% endfor %}
-
});
</script>
-
{% endblock extrajs %} \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/resource_detail.html b/tools/pharos-dashboard/templates/dashboard/resource_detail.html
new file mode 100644
index 00000000..4fba4766
--- /dev/null
+++ b/tools/pharos-dashboard/templates/dashboard/resource_detail.html
@@ -0,0 +1,64 @@
+<div class="row">
+ <div class="col-lg-3">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Utilization
+ </div>
+ <div class="panel-body">
+ <div class="flot-chart">
+ <div class="flot-chart-content" id="{{ resource.id }}_slave_utilization"></div>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-9">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Servers
+ </div>
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover"
+ id="{{ resource.id }}_server_table" cellspacing="0"
+ width="100%">
+ {% include "dashboard/server_table.html" %}
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+<div class="row">
+ <div class="col-lg-6">
+ <div class="panel panel-default">
+ <div class="panel-heading">
+ Bookings
+ </div>
+ <div class="panel-body">
+ <div class="dataTables_wrapper">
+ <table class="table table-striped table-bordered table-hover"
+ id="{{ resource.id }}_bookings_table" cellspacing="0"
+ width="100%">
+ {% include "booking/booking_table.html" %}
+ </table>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
+
+<script type="text/javascript">
+ var data_{{ resource.id }} = [{
+ label: "Offline",
+ data: {{ utilization.offline }},
+ color: '#d9534f'
+ }, {
+ label: "Online",
+ data: {{ utilization.online }},
+ color: '#5cb85c'
+ }, {
+ label: "Idle",
+ data: {{ utilization.idle }},
+ color: '#5bc0de'
+ }];
+</script> \ No newline at end of file
diff --git a/tools/pharos-dashboard/templates/dashboard/server_table.html b/tools/pharos-dashboard/templates/dashboard/server_table.html
new file mode 100644
index 00000000..d47e5204
--- /dev/null
+++ b/tools/pharos-dashboard/templates/dashboard/server_table.html
@@ -0,0 +1,30 @@
+<thead>
+<tr>
+ <th>Server</th>
+ <th>Model</th>
+ <th>CPU</th>
+ <th>RAM</th>
+ <th>Storage</th>
+</tr>
+</thead>
+<tbody>
+{% for server in resource.server_set.all %}
+ <tr>
+ <th>
+ {{ server.name }}
+ </th>
+ <th>
+ {{ server.model }}
+ </th>
+ <th>
+ {{ server.cpu }}
+ </th>
+ <th>
+ {{ server.ram }}
+ </th>
+ <th>
+ {{ server.storage }}
+ </th>
+ </tr>
+{% endfor %}`
+</tbody> \ No newline at end of file