Commit b8be2f29 authored by Oleg Borisenko's avatar Oleg Borisenko
Browse files

targets can be added via web

parent 56777dc7
......@@ -8,12 +8,16 @@ def includeme(config):
config.add_route('library_scan', '/library_scan')
config.add_route('add_backup_target', '/add_backup_target')
config.add_route('add_backup_target_post', '/add_backup_target_post')
config.add_route('add_restore_target', '/add_restore_target')
config.add_route('add_restore_target_post', '/add_restore_target_post')
config.add_route('scan_backup_target', '/scan_backup_target/{unique_label}')
config.add_route('identify_tape', '/identify_tape')
config.add_route('list_tapes', '/list_tapes')
config.add_route('export_tape', '/export_tape')
config.add_route('import_tape', '/import_tape')
config.add_route('list_backup_targets', '/list_backup_targets')
config.add_route('list_restore_targets', '/list_restore_targets')
config.add_route('test_behavior', '/test_behavior')
config.add_route('copy_status', '/copy_status')
config.add_route('batches', '/batches')
......
......@@ -128,6 +128,10 @@ body {
font-size: x-small;
}
.restore_targets {
font-size: x-small;
}
@media (max-width: 768px) {
.row {
flex-wrap: wrap;
......
......@@ -33,7 +33,8 @@
class="navbar-menu-link"
id="tapes">Обзор
кассет</a>
<a href="/files" class="navbar-menu-link" id="files">Поиск файлов</a>
<a href="/files" class="navbar-menu-link" id="files">Обзор файлов</a>
<a href="/labnums" class="navbar-menu-link" id="labnums">Обзор лаб.номеров</a>
</nav>
</div>
</div>
......
{% extends "base.jinja2" %}
{% block content %}
<p>Вы можете выбрать либо изначальную цель бэкапа для полного восстановления, либо конкретный образец; в случае
конфликта приоритет у образца</p>
<form class="form" id="export_tapes" action="/export_tapes" method="post">
<div class="form-group" id="files_filter">
<label class="label">Вбейте лабномер для фильтрации</label>
<input placeholder="Лабномер" class="input"/>
<p>Вы можете выбрать параметры фильтрации для восстановления;
результирующий список будет пересечением указанных параметров</p>
<form class="form" id="filter_files" action="/filter_files" method="post">
<div class="form-group">
<label class="label">Вбейте лабномера через запятую или их подстроки для фильтрации</label>
<input placeholder="Лабномер" class="input" name="labnum"/>
</div>
<select class="select">
<option value="">- Выберите изначальное хранилище для полного восстановления -</option>
</select>
<select class="select">
<option value="">Нет доступных целей восстановления</option>
<select class="select" name="backup_target">
<option value="">- Выберите изначальную цель бэкапа для восстановления -</option>
{% for target in backup_targets %}
<option value="{{ target['unique_label'] }}">
{% endfor %}
</select>
<div class="form-group">
<button type="submit" class="button">Отфильтровать</button>
......
{% extends "base.jinja2" %}
{% block content %}
<p>Вы можете выбрать либо изначальную цель бэкапа для полного восстановления, либо конкретный образец; в случае
конфликта приоритет у образца</p>
<form class="form" id="export_tapes" action="/export_tapes" method="post">
<div class="form-group" id="files_filter">
<label class="label">Вбейте лабномер для фильтрации</label>
<input placeholder="Лабномер" class="input"/>
</div>
<select class="select">
<option value="">- Выберите изначальное хранилище для полного восстановления -</option>
</select>
<select class="select">
<option value="">Нет доступных целей восстановления</option>
</select>
<div class="form-group">
<button type="submit" class="button">Отфильтровать</button>
</div>
</form>
<table class="files">
<thead>
<tr>
<td>
Скопировано на кассету
</td>
<td>
Уникальный ID цели
</td>
<td>
Тип
</td>
<td>
Размер (Гбайт)
</td>
<td>
Чексумма
</td>
<td>
Последнее изменение оригинального файла
</td>
<td>
Время бэкапа
</td>
<td>
Путь
</td>
</tr>
</thead>
<tbody>
{% for f in files %}
<tr>
<td>{{ f.tape_label }}</td>
<td>{{ f.target_unique_label }}</td>
<td>{{ f.kind }}</td>
<td>{{ f.fsize / 1024**3 | round(2) }}</td>
<td>{{ f.checksum }}</td>
<td>{{ f.file_mtime }}</td>
<td>{{ f.copied_at_time }}</td>
<td>{{ f.relative_path }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<form>
<div class="form-group">
<button type="submit" class="button">Восстановить попадающее под фильтр на выбранную цель восстановления</button>
</div>
</form>
{% endblock %}
\ No newline at end of file
......@@ -10,18 +10,19 @@
<div class="cell" style="flex-basis: 60%">
<div>Цели бэкапа
</div>
<form class="form" id="backup_unique_label" action="/add_backup_target" method="post">
<form class="form" id="backup_unique_label" action="/add_backup_target_post" method="post">
<div class="form-group">
<label class="label">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<input placeholder="Вбейте уникальный идентификатор цели бэкапа" class="input" id="i3f4c"/>
<input name="unique_label" placeholder="Вбейте уникальный идентификатор цели бэкапа" class="input"/>
</div>
<div class="form-group" id="backup_local_fullpath">
<label class="label">Полный путь</label>
<input type="email" placeholder="Вбейте валидный и существующий локальный путь в ФС сервера backups" class="input"/>
<input name="fullpath" placeholder="Вбейте валидный и существующий локальный путь в ФС сервера backups" class="input"/>
</div>
<div class="form-group" id="backup_period_selector">
<label class="label">Период сканирования</label>
<select class="select"><option value="" id="itae1">- Выберите период -</option>
<select name="rescan_interval" class="select">
<option value="">- Выберите период -</option>
<option value="1">Раз в сутки</option>
<option value="2">Раз в двое суток</option>
<option value="7">Раз в неделю</option>
......@@ -30,7 +31,7 @@
</div>
<div class="form-group">
<label class="label">Тип сканируемой цели (лишние файлы относительно типа игнорируются)</label>
<select class="select">
<select name="kind" class="select">
<option value="">- Выберите тип -</option>
<option value="wgs">Полногеномные данные</option>
<option value="vcf">Данные VCF</option>
......@@ -69,17 +70,17 @@
</div>
</div>
</div>
<div class="cell" id="iwxir" style="flex-basis: 40%">
<div id="iqvor">Цели восстановления
<div class="cell" style="flex-basis: 40%">
<div>Цели восстановления
</div>
<form class="form">
<form class="form" action="/add_restore_target_post" method="post">
<div class="form-group">
<label class="label">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<input placeholder="Дайте уникальное имя цели восстановления" class="input"/>
<input placeholder="Дайте уникальное имя цели восстановления" name="unique_label" class="input"/>
</div>
<div class="form-group">
<label class="label">Полный путь к точке монтирования цели восстановления</label>
<input placeholder="Точка монтирования" class="input"/>
<input placeholder="Точка монтирования" name="fullpath" class="input"/>
</div>
<div class="form-group">
<button type="submit" class="button">Зарегистрировать цель восстановления</button>
......@@ -87,6 +88,20 @@
</form>
<div class="row">
<div class="cell">
<table class="restore_targets">
<th>Уникальная метка</th>
<th>Полный путь</th>
<th>Свободного места (Гб)</th>
<th>Включена?</th>
{% for target in restore_targets %}
<tr>
<td>{{ target['unique_label'] }}</td>
<td>{{ target['fullpath'] }}</td>
<td>{{ target['df_gb'] }}</td>
<td>{{ target['enabled'] }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
......
from pyramid.view import view_config
from pyramid.response import Response
from pyramid.request import Request
from pyramid.httpexceptions import HTTPException
from pyramid.httpexceptions import HTTPException, HTTPFound
from .. import utils
from .. import models
......@@ -102,10 +102,59 @@ def targets_route(request):
try:
subreq = Request.blank('/list_backup_targets')
backup_targets = request.invoke_subrequest(subreq, use_tweens=True).json
subreq = Request.blank('/list_restore_targets')
restore_targets = request.invoke_subrequest(subreq, use_tweens=True).json
except HTTPException as e:
return Response(json_body={"error": e.detail}, content_type='application/json', status=e.status)
return {"backup_targets": backup_targets}
return {"backup_targets": backup_targets, "restore_targets": restore_targets}
@view_config(route_name='add_backup_target_post')
def add_backup_target_post(request):
try:
unique_label = request.POST.get("unique_label")
fullpath = request.POST.get("fullpath")
rescan_interval = request.POST.get("rescan_interval")
kind = request.POST.get("kind")
if not unique_label or not fullpath or not rescan_interval or not kind:
raise HTTPException("All parameters are obligatory")
subreq = Request.blank('/add_backup_target', method='POST',
json={"unique_label": unique_label,
"fullpath": fullpath,
"rescan_interval": rescan_interval * 1440,
"kind": kind})
res = request.invoke_subrequest(subreq, use_tweens=True).json
next_url = request.route_url('targets',
_query=(('result', 'Backup target %s successfully added' % (unique_label)),
('color', 'green')))
return HTTPFound(location=next_url)
except HTTPException as e:
next_url = request.route_url('targets',
_query=(('result', e.detail),
('color', 'red')))
return HTTPFound(location=next_url)
@view_config(route_name='add_restore_target_post')
def add_restore_target_post(request):
try:
unique_label = request.POST.get("unique_label")
fullpath = request.POST.get("fullpath")
if not unique_label or not fullpath:
raise HTTPException("All parameters are obligatory")
subreq = Request.blank('/add_restore_target', method='POST',
json={"unique_label": unique_label,
"fullpath": fullpath})
res = request.invoke_subrequest(subreq, use_tweens=True).json
next_url = request.route_url('targets',
_query=(('result', 'Restore target %s successfully added' % (unique_label)),
('color', 'green')))
return HTTPFound(location=next_url)
except HTTPException as e:
next_url = request.route_url('targets',
_query=(('result', e.detail),
('color', 'red')))
return HTTPFound(location=next_url)
@view_config(route_name='tapes', renderer='tapebackup:templates/tapes.jinja2')
def tapes_route(request):
......
import datetime
import shutil
import re
import os.path
from pyramid.view import view_config
......@@ -30,6 +32,22 @@ def add_backup_target(request):
return Response(json_body={"error": e.detail}, content_type='application/json', status=e.status)
return
@view_config(route_name='add_restore_target', renderer='json', request_method='POST')
def add_restore_target(request):
try:
target_props = dict((k, request.json_body[k]) for k in ('unique_label', 'fullpath'))
target_props['fullpath'] = os.path.normpath(target_props['fullpath'])
if not re.match("^\w+$", target_props['unique_label']):
raise HTTPException("Allowed labels are only alphanumeric and _ characters")
target_props['enabled'] = True
new_target = models.restoretarget.RestoreTarget(**target_props)
request.dbsession.add(new_target)
except SQLAlchemyError as e:
return Response(json_body={"error": e._message()}, content_type='application/json', status=500)
except HTTPException as e:
return Response(json_body={"error": e.detail}, content_type='application/json', status=e.status)
return
@view_config(route_name='list_backup_targets', renderer='json', request_method='GET')
def list_backup_targets(request):
try:
......@@ -49,3 +67,20 @@ def list_backup_targets(request):
return Response(json_body={"error": e._message()}, content_type='application/json', status=500)
except HTTPException as e:
return Response(json_body={"error": e.detail}, content_type='application/json', status=e.status)
@view_config(route_name='list_restore_targets', renderer='json', request_method='GET')
def list_restore_targets(request):
try:
targets = [x for x in request.dbsession.query(models.restoretarget.RestoreTarget).all()]
target_to_return = []
for target in targets:
total, used, free = shutil.disk_usage(target.fullpath)
target_to_ret = target.to_dict()
target_to_ret['df_gb'] = round(free / (1024**3), 2)
target_to_return.append(target_to_ret)
return target_to_return
except SQLAlchemyError as e:
return Response(json_body={"error": e._message()}, content_type='application/json', status=500)
except HTTPException as e:
return Response(json_body={"error": e.detail}, content_type='application/json', status=e.status)
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment