summaryrefslogtreecommitdiffstats
path: root/ogcp
diff options
context:
space:
mode:
Diffstat (limited to 'ogcp')
-rw-r--r--ogcp/forms/action_forms.py9
-rw-r--r--ogcp/templates/actions/cache.html192
-rw-r--r--ogcp/templates/commands.html2
-rw-r--r--ogcp/views.py99
4 files changed, 301 insertions, 1 deletions
diff --git a/ogcp/forms/action_forms.py b/ogcp/forms/action_forms.py
index 46081b5..19387aa 100644
--- a/ogcp/forms/action_forms.py
+++ b/ogcp/forms/action_forms.py
@@ -87,6 +87,15 @@ class SessionForm(FlaskForm):
ips = HiddenField()
os = RadioField(label=_l('Session'), choices=[])
+class CacheImage(FlaskForm):
+ selected = BooleanField()
+ image_name = HiddenField()
+ clients = HiddenField()
+
+class CacheForm(FlaskForm):
+ ips = HiddenField()
+ images = FieldList(FormField(CacheImage))
+
class ImageRestoreForm(FlaskForm):
ips = HiddenField()
partition = SelectField(label=_l('Partition'), choices=[])
diff --git a/ogcp/templates/actions/cache.html b/ogcp/templates/actions/cache.html
new file mode 100644
index 0000000..07f5b16
--- /dev/null
+++ b/ogcp/templates/actions/cache.html
@@ -0,0 +1,192 @@
+{% extends 'commands.html' %}
+{% import "bootstrap/wtf.html" as wtf %}
+
+{% set sidebar_state = 'disabled' %}
+{% set btn_back = true %}
+
+{% block nav_client %} active{% endblock %}
+{% block nav_client_cache %} active{% endblock %}
+{% block content %}
+
+{% set ip_list = form.ips.data.split(' ') %}
+{% set ip_count = ip_list | length %}
+<h1 class="m-5">
+ {{ _('Manage Cache') }}
+</h1>
+
+<p>{{ _('Check free cache space in the client\'s bubbles:') }}</p>
+
+{{ macros.cmd_selected_clients(selected_clients) }}
+
+<p>{{ _('Select the images to be deleted:') }}</p>
+
+<form class="form-inline" method="POST" id="cacheForm">
+ <table class="table table-hover">
+ <thead class="thead-light">
+ <tr>
+ <th>{{ _('Image') }}</th>
+ <th>{{ _('Clients') }}</th>
+ </tr>
+ </thead>
+
+ <tbody data-target="cache-fieldset" id="cacheTable" class="text-left">
+ {{ form.hidden_tag() }}
+ {% for image in form.images %}
+ <tr data-toggle="fieldset-entry">
+ <td class="radio-container">
+ {{ image.selected(class_="form-control") }}
+ {{ image.image_name() }}
+ {{ image.clients() }}
+ <b>{{ image.image_name.data }} ({{ (images_data[image.selected.label.text]['size'] | int / 2**20)|round(3) }} MiB)</b>
+ </td>
+ <td>{{ ', '.join(images_data[image.selected.label.text]['clients']) }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ <button class="btn btn-danger" form="cacheForm">
+ {{ _('Delete') }}
+ </button>
+</form>
+
+<br>
+
+<div class="card text-center">
+ <div class="card-header">
+ {{ _('Detailed cache usage') }}
+ </div>
+ <div class="card-body">
+ <label for="cacheSelect">Choose a client:</label>
+ <select id="cacheSelect" onchange="onClientSelected()">
+ {% for client_ip in ip_list %}
+ <option value="{{ client_ip }}">{{ client_ip }}</option>
+ {% endfor %}
+ </select>
+
+ <ul class="list-group list-group-horizontal">
+ <li class="list-group-item w-50">
+ <canvas id="cacheChart" class="mb-2"></canvas>
+ </li>
+ <li class="list-group-item w-50">
+ <p>{{ _('Images in cache:') }}</p>
+ <div id="cacheList"></div>
+ </li>
+ </ul>
+ <ul class="list-group list-group-horizontal">
+ <li class="list-group-item w-50">
+ {{ _('Disk size') }}
+ </li>
+ <li class="list-group-item w-50">
+ {{ _('used') }} (%)
+ </li>
+ <li class="list-group-item w-50">
+ {{ _('available') }} (%)
+ </li>
+ </ul>
+ <ul class="list-group list-group-horizontal">
+ <li id="totalCacheItem" class="list-group-item w-50"></li>
+ <li id="usedCacheItem" class="list-group-item w-50"></li>
+ <li id="freeCacheItem" class="list-group-item w-50"></li>
+ </ul>
+ </div>
+ </div>
+
+<!-- jQuery -->
+<script src="{{ url_for('static', filename='AdminLTE/plugins/jquery/jquery.min.js') }}"></script>
+<!-- ChartJS -->
+<script src="{{ url_for('static', filename='AdminLTE/plugins/chart.js/Chart.min.js') }}"></script>
+<script>
+ var cacheChartConfig = {
+ type: 'doughnut',
+ data: {
+ labels: ['Used', 'Available'],
+ datasets: [
+ {
+ label: 'Disk usage',
+ data: [
+ 0,
+ 1,
+ ],
+ backgroundColor: [
+ 'rgb(255, 99, 132)',
+ 'rgb(54, 162, 235)',
+ ],
+ },
+ ],
+ },
+ options: {
+ responsive: true,
+ plugins: {
+ legend: {
+ position: 'top',
+ },
+ title: {
+ display: true,
+ text: 'Chart.js Doughnut Chart'
+ },
+ },
+ },
+ };
+ var cacheChart = new Chart(
+ document.getElementById('cacheChart'),
+ cacheChartConfig,
+ );
+
+ var storageData = {{ storage_data|tojson|safe }};
+ var imageData = {{ images_data|tojson|safe }};
+ var clientImages = {{ client_images|tojson|safe }};
+
+ function onClientSelected() {
+ var selectElement = document.getElementById("cacheSelect");
+ var selectedOption = selectElement.options[selectElement.selectedIndex].text;
+ updateChart(selectedOption);
+ }
+
+ function toGiB(v, decimals) {
+ return (v / Math.pow(2, 30)).toFixed(decimals);
+ }
+
+ function updateChart(ip) {
+ var totalCache = toGiB(storageData[ip].total, 3);
+ var usedCache = toGiB(storageData[ip].used, 3);
+ var freeCache = toGiB(storageData[ip].total - storageData[ip].used, 3)
+
+ cacheChart.data.datasets[0].data = [
+ usedCache,
+ freeCache,
+ ]
+ cacheChart.update();
+
+ var totalCacheItem = document.getElementById("totalCacheItem");
+ totalCacheItem.innerHTML = totalCache + " GiB";
+
+ var usedCacheItem = document.getElementById("usedCacheItem");
+ usedCacheItem.innerHTML = usedCache + " GiB (" + Math.round((usedCache / totalCache) * 100) + "%)";
+
+ var freeCacheItem = document.getElementById("freeCacheItem");
+ freeCacheItem.innerHTML = freeCache + " GiB (" + Math.round((freeCache / totalCache) * 100) + "%)";
+
+ var cacheList = document.getElementById("cacheList");
+ cacheList.innerHTML = "";
+ clientImages[ip].forEach(function(img) {
+ cacheList.innerHTML += imageData[img]["name"] + " (" + (imageData[img]["size"] / Math.pow(2, 20)).toFixed(3) + " MiB)<br>";
+ });
+ }
+
+ updateChart("{{ ip_list[0] }}");
+
+ // Update pill data
+ $('.badge-pill').each(function(index) {
+ for (var ip in storageData) {
+ if ($(this).html().includes(ip)) {
+ var totalCache = storageData[ip].total;
+ var usedCache = storageData[ip].used;
+ var freeCache = toGiB(totalCache - usedCache, 1)
+ $(this).html($(this).html() + '<br>free: ' + freeCache + ' GiB');
+ break;
+ }
+ }
+ });
+</script>
+
+{% endblock %}
diff --git a/ogcp/templates/commands.html b/ogcp/templates/commands.html
index 92a1284..316eb8f 100644
--- a/ogcp/templates/commands.html
+++ b/ogcp/templates/commands.html
@@ -28,6 +28,8 @@
form="scopesForm" formaction="{{ url_for('action_session') }}" formmethod="get">
<input class="btn btn-light dropdown-item{% block nav_client_details %}{% endblock %}" type="submit" value="{{ _('Client details') }}"
form="scopesForm" formaction="{{ url_for('action_client_info') }}" formmethod="get">
+ <input class="btn btn-light dropdown-item{% block nav_client_cache %}{% endblock %}" type="submit" value="{{ _('Manage cache') }}"
+ form="scopesForm" formaction="{{ url_for('action_client_cache') }}" formmethod="get">
</div>
</div>
diff --git a/ogcp/views.py b/ogcp/views.py
index d02359b..32beb3e 100644
--- a/ogcp/views.py
+++ b/ogcp/views.py
@@ -13,7 +13,7 @@ from ogcp.forms.action_forms import (
SessionForm, ImageRestoreForm, ImageCreateForm, SoftwareForm, BootModeForm,
RoomForm, DeleteRoomForm, CenterForm, DeleteCenterForm, OgliveForm,
GenericForm, SelectClientForm, ImageUpdateForm, ImportClientsForm,
- ServerForm, DeleteRepositoryForm, RepoForm, FolderForm
+ ServerForm, DeleteRepositoryForm, RepoForm, FolderForm, CacheForm
)
from flask_login import (
current_user, LoginManager,
@@ -1033,6 +1033,103 @@ def action_session():
selected_clients=selected_clients,
scopes=scopes, os_groups=os_groups)
+@app.route('/action/cache', methods=['GET', 'POST'])
+@login_required
+def action_client_cache():
+ form = CacheForm(request.form)
+ if request.method == 'POST':
+ ips = form.ips.data.split(' ')
+ server = get_server_from_clients(list(ips))
+
+ client_list = []
+ image_list = []
+ for entry in form.images.entries:
+ if not entry.selected.data:
+ continue
+
+ image_list.append(entry.image_name.data)
+ for client_ip in entry.clients.data.split(' '):
+ if not client_ip in client_list:
+ client_list.append(client_ip)
+
+ if not image_list:
+ flash(_(f'No selected images to delete'), category='error')
+ return redirect(url_for('commands'))
+
+ r = server.post('/cache/delete',
+ payload={'clients': client_list,
+ 'images': image_list})
+ if r.status_code == requests.codes.ok:
+ flash(_('Cache delete request sent successfully'), category='info')
+ else:
+ flash(_(f'Invalid cache delete form'), category='error')
+ return redirect(url_for('commands'))
+ else:
+ ips = parse_elements(request.args.to_dict())
+ ips_list = list(ips)
+ if not validate_elements(ips):
+ return redirect(url_for('commands'))
+
+ server = get_server_from_clients(ips_list)
+ form.ips.data = ' '.join(ips_list)
+
+ r = server.get('/cache/list', payload={'clients': ips_list})
+ if not r:
+ return ogserver_down('commands')
+ if r.status_code != requests.codes.ok:
+ return ogserver_error('commands')
+
+ clients_info = r.json()['clients']
+
+ if not clients_info:
+ flash(_('ogServer returned an empty client list'), category='error')
+ return redirect(url_for('commands'))
+
+ storage_data = {}
+ images_data = {}
+ client_images = {}
+ for client_info in clients_info:
+ ip = client_info['ip']
+ cache_size = int(client_info['cache_size'])
+ used_cache = 0
+ for image_info in client_info['images']:
+ image_name = image_info['name']
+ checksum = image_info['checksum']
+ used_cache += int(image_info['size'])
+ img_identifier = f'{image_name}.{checksum}'
+
+ if ip in client_images:
+ client_images[ip].append(img_identifier)
+ else:
+ client_images[ip] = [img_identifier]
+
+ if img_identifier in images_data:
+ images_data[img_identifier]['clients'].append(ip)
+ else:
+ images_data[img_identifier] = {
+ 'clients': [ip],
+ 'size': int(image_info['size']),
+ 'name': image_name,
+ }
+
+ storage_data[ip] = {'used': used_cache,
+ 'total': cache_size}
+
+ for img_identifier in images_data:
+ image_data = images_data[img_identifier]
+ checkbox_entry = form.images.append_entry()
+ checkbox_entry.selected.label.text = img_identifier
+ checkbox_entry.image_name.data = image_data['name']
+ checkbox_entry.clients.data = ' '.join(image_data['clients'])
+
+ scopes, clients = get_scopes(set(ips))
+ selected_clients = list(get_selected_clients(scopes['scope']).items())
+ return render_template('actions/cache.html', form=form,
+ selected_clients=selected_clients,
+ scopes=scopes, images_data=images_data,
+ storage_data=storage_data,
+ client_images=client_images)
+
@app.route('/action/client/info', methods=['GET'])
@login_required
def action_client_info():