diff options
-rw-r--r-- | ogcp/forms/action_forms.py | 9 | ||||
-rw-r--r-- | ogcp/templates/actions/cache.html | 192 | ||||
-rw-r--r-- | ogcp/templates/commands.html | 2 | ||||
-rw-r--r-- | ogcp/views.py | 99 |
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(): |