Commit 2ef5e1dc authored by Oleg Borisenko's avatar Oleg Borisenko
Browse files

making views for backups UI

parent 915a4f3f
......@@ -2,10 +2,13 @@ def includeme(config):
config.add_static_view('static', 'static', cache_max_age=3600)
config.add_route('home', '/')
config.add_route('status', '/status')
config.add_route('targets', '/targets')
config.add_route('tapes', '/tapes')
config.add_route('library_scan', '/library_scan')
config.add_route('add_backup_target', '/add_backup_target')
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('use_tape_for_backup', '/use_tape_for_backup')
config.add_route('use_tape_for_restore', '/use_tape_for_restore')
config.add_route('list_backup_targets', '/list_backup_targets')
......
......@@ -120,6 +120,10 @@ body {
font-size: x-small;
}
.backup_targets {
font-size: x-small;
}
@media (max-width: 768px) {
.row {
flex-wrap: wrap;
......@@ -141,4 +145,49 @@ body {
.navbar-burger {
display: block;
}
}
.form{
border-radius:3px;
padding:10px 15px;
background-color:rgba(0,0,0,0.2);
}
.input{
width:100%;
margin-bottom:15px;
padding:7px 10px;
border-radius:2px;
color:#fff;
background-color:#554c57;
border:none;
}
.select{
width:100%;
margin-bottom:15px;
padding:7px 10px;
border-radius:2px;
color:#fff;
background-color:#554c57;
border:none;
height:30px;
}
.label{
width:100%;
display:block;
}
.button{
width:100%;
margin:15px 0;
background-color:#785580;
border:none;
color:#fff;
border-radius:2px;
padding:7px 10px;
font-size:1em;
cursor:pointer;
}
*{
box-sizing:border-box;
}
\ No newline at end of file
......@@ -5,7 +5,6 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Tapebackup management UI">
<link rel="shortcut icon" href="{{request.static_url('tapebackup:static/pyramid-16x16.png')}}">
<title>Tapebackup UI</title>
......@@ -30,12 +29,12 @@
<a href="/" class="navbar-menu-link" id="help">Описание</a>
<a href="/status" class="navbar-menu-link" id="status">Статус
системы</a>
<a href="/" class="navbar-menu-link" id="targets">Цели бэкапа и восстановления</a>
<a href="/"
<a href="/targets" class="navbar-menu-link" id="targets">Цели бэкапа и восстановления</a>
<a href="/tapes"
class="navbar-menu-link"
id="tapes">Обзор
кассет</a>
<a href="/" class="navbar-menu-link" id="files">Поиск файлов</a>
<a href="/files" class="navbar-menu-link" id="files">Поиск файлов</a>
</nav>
</div>
</div>
......@@ -44,4 +43,42 @@
{% block content %}{% endblock %}
</body>
<script>var items = document.querySelectorAll('#iwiimc');
for (var i = 0, len = items.length; i < len; i++) {
(function () {
var e, t = 0, n = function () {
var e, t = document.createElement("void"), n = {
transition: "transitionend",
OTransition: "oTransitionEnd",
MozTransition: "transitionend",
WebkitTransition: "webkitTransitionEnd"
};
for (e in n) if (void 0 !== t.style[e]) return n[e]
}(), r = function (e) {
var t = window.getComputedStyle(e), n = t.display,
r = (t.position, t.visibility, t.height, parseInt(t["max-height"]));
if ("none" !== n && "0" !== r) return e.offsetHeight;
e.style.height = "auto", e.style.display = "block", e.style.position = "absolute", e.style.visibility = "hidden";
var i = e.offsetHeight;
return e.style.height = "", e.style.display = "", e.style.position = "", e.style.visibility = "", i
}, i = function (e) {
t = 1;
var n = r(e), i = e.style;
i.display = "block", i.transition = "max-height 0.25s ease-in-out", i.overflowY = "hidden", "" == i["max-height"] && (i["max-height"] = 0), 0 == parseInt(i["max-height"]) ? (i["max-height"] = "0", setTimeout(function () {
i["max-height"] = n + "px"
}, 10)) : i["max-height"] = "0"
}, a = function (r) {
if (r.preventDefault(), !t) {
var a = this.closest("[data-gjs=navbar]"), o = a.querySelector("[data-gjs=navbar-items]");
i(o), e || (o.addEventListener(n, function () {
t = 0;
var e = o.style;
0 == parseInt(e["max-height"]) && (e.display = "", e["max-height"] = "")
}), e = 1)
}
};
"gjs-collapse" in this || this.addEventListener("click", a), this["gjs-collapse"] = 1
}.bind(items[i]))();
}</script>
</html>
......@@ -2,9 +2,9 @@
{% block content %}
<div class="row">
<div class="cell">
<div class="cell" style="flex-basis: 70%">
<section class="bdg-sect" id="ignhy">
<h1 class="heading"><i>Надеюсь, вы здесь из любопытства, а не из-за катастрофы</i>
<h1 class="heading">Описание. <i>Надеюсь, вы здесь из любопытства, а не из-за катастрофы</i>
</h1>
<h2 class="heading">Список допустимых действий в системе:
</h2>
......@@ -31,39 +31,38 @@
</div>
</section>
</div>
<div class="cell" style="flex-basis: 30%">
<section class="bdg-sect">
<h2>Краткая статистика</h2>
<div class="cell">
<p class="paragraph">Процесс системы резервирования: {% if copy_status['backup_is_running'] %} запущен{% else %} не запущен{% endif %}</p>
<p class="paragraph">Процесс системы восстановления: {% if copy_status['restore_is_running'] %} запущен{% else %} не запущен{% endif %}</p>
{% if copy_status['backup_is_running'] and copy_status['restore_is_running'] %}
<p class="paragraph">ВНИМАНИЕ: запускать систему резервирования и восстановления одновременно ОЧЕНЬ ОПАСНО</p>
{% endif %}
<p class="paragraph">Данных в целях бэкапа:</p>
<ul>
<li>файлов - {{ copy_status['total_files'] }}</li>
<li>суммарный объем - {{ copy_status['total_gb'] }} Гбайт</li>
<li>из них wgs - {{ copy_status['total_wgs_gb'] }} Гбайт</li>
</ul>
<p class="paragraph">Данных скопировано на кассеты:</p>
<ul>
<li>файлов - {{ copy_status['copied_files'] }}</li>
<li>суммарная емкость - {{ copy_status['copied_gb'] }} Гбайт</li>
<li>из них wgs - {{ copy_status['copied_wgs_gb'] }} Гбайт</li>
<li>лабораторных номеров - TODO</li>
</ul>
<p class="paragraph">Кассет:</p>
<ul>
<li>записано всего -  </li>
<li>вне ленточной библиотеки -</li>
<li>внутри ленточной библиотеки - </li>
<li>готовых к изъятию и перевозке - </li>
</ul>
</div>
</section>
</div>
</div>
<script>var items = document.querySelectorAll('#iwiimc');
for (var i = 0, len = items.length; i < len; i++) {
(function(){
var e,t=0,n=function(){
var e,t=document.createElement("void"),n={
transition:"transitionend",OTransition:"oTransitionEnd",MozTransition:"transitionend",WebkitTransition:"webkitTransitionEnd"};
for(e in n)if(void 0!==t.style[e])return n[e]}
(),r=function(e){
var t=window.getComputedStyle(e),n=t.display,r=(t.position,t.visibility,t.height,parseInt(t["max-height"]));
if("none"!==n&&"0"!==r)return e.offsetHeight;
e.style.height="auto",e.style.display="block",e.style.position="absolute",e.style.visibility="hidden";
var i=e.offsetHeight;
return e.style.height="",e.style.display="",e.style.position="",e.style.visibility="",i}
,i=function(e){
t=1;
var n=r(e),i=e.style;
i.display="block",i.transition="max-height 0.25s ease-in-out",i.overflowY="hidden",""==i["max-height"]&&(i["max-height"]=0),0==parseInt(i["max-height"])?(i["max-height"]="0",setTimeout(function(){
i["max-height"]=n+"px"}
,10)):i["max-height"]="0"}
,a=function(r){
if(r.preventDefault(),!t){
var a=this.closest("[data-gjs=navbar]"),o=a.querySelector("[data-gjs=navbar-items]");
i(o),e||(o.addEventListener(n,function(){
t=0;
var e=o.style;
0==parseInt(e["max-height"])&&(e.display="",e["max-height"]="")}
),e=1)}
};
"gjs-collapse"in this||this.addEventListener("click",a),this["gjs-collapse"]=1
}
.bind(items[i]))();
}
</script>
{% endblock content %}
\ No newline at end of file
{% extends "base.jinja2" %}
{% block content %}
<section class="bdg-sect" id="ignhy">
<h1 class="heading">Статус системы
</h1>
</section>
<div class="row">
<div class="cell" id="ixzf5m">
<div id="il4534">Статус системы копирования</div>
<div id="i9hs3h">Процесс системы резервирования: запущен</div>
<div id="iy3wqs">Процесс системы восстановления: остановлен</div>
<div id="ic3w0b">Данных в целях бэкапа: 
<div>файлов - </div>
<div>суммарный объем -</div>
</div>
<div id="irto82">Данных скопировано на кассеты:
<div>файлов - </div>
<div>суммарная емкость - </div>
<div>лабораторных номеров - </div>
</div>
<div id="i2q7h9">Кассет:
<div>записано всего - </div>
<div>вне ленточной библиотеки -</div>
<div>внутри ленточной библиотеки - </div>
<div>готовых к изъятию и перевозке - </div>
</div>
<div class="cell">
<p class="paragraph">Процесс системы резервирования: {% if copy_status['backup_is_running'] %} запущен{% else %} не запущен{% endif %}</p>
<p class="paragraph">Процесс системы восстановления: {% if copy_status['restore_is_running'] %} запущен{% else %} не запущен{% endif %}</p>
{% if copy_status['backup_is_running'] and copy_status['restore_is_running'] %}
<p class="paragraph">ВНИМАНИЕ: запускать систему резервирования и восстановления одновременно ОЧЕНЬ ОПАСНО</p>
{% endif %}
<p class="paragraph">Данных в целях бэкапа:</p>
<ul>
<li>файлов - {{ copy_status['total_files'] }}</li>
<li>суммарный объем - {{ copy_status['total_gb'] }} Гбайт</li>
<li>из них wgs - {{ copy_status['total_wgs_gb'] }} Гбайт</li>
</ul>
<p class="paragraph">Данных скопировано на кассеты:</p>
<ul>
<li>файлов - {{ copy_status['copied_files'] }}</li>
<li>суммарная емкость - {{ copy_status['copied_gb'] }} Гбайт</li>
<li>из них wgs - {{ copy_status['copied_wgs_gb'] }} Гбайт</li>
<li>лабораторных номеров - TODO</li>
</ul>
<p class="paragraph">Кассет:</p>
<ul>
<li>записано всего -  </li>
<li>вне ленточной библиотеки -</li>
<li>внутри ленточной библиотеки - </li>
<li>готовых к изъятию и перевозке - </li>
</ul>
</div>
<div class="cell" id="icz0xd">
<div id="iifin7">Статус ленточной библиотеки</div>
......@@ -213,41 +224,4 @@
</table>
</div>
</div>
<script>var items = document.querySelectorAll('#iwiimc');
for (var i = 0, len = items.length; i < len; i++) {
(function () {
var e, t = 0, n = function () {
var e, t = document.createElement("void"), n = {
transition: "transitionend",
OTransition: "oTransitionEnd",
MozTransition: "transitionend",
WebkitTransition: "webkitTransitionEnd"
};
for (e in n) if (void 0 !== t.style[e]) return n[e]
}(), r = function (e) {
var t = window.getComputedStyle(e), n = t.display,
r = (t.position, t.visibility, t.height, parseInt(t["max-height"]));
if ("none" !== n && "0" !== r) return e.offsetHeight;
e.style.height = "auto", e.style.display = "block", e.style.position = "absolute", e.style.visibility = "hidden";
var i = e.offsetHeight;
return e.style.height = "", e.style.display = "", e.style.position = "", e.style.visibility = "", i
}, i = function (e) {
t = 1;
var n = r(e), i = e.style;
i.display = "block", i.transition = "max-height 0.25s ease-in-out", i.overflowY = "hidden", "" == i["max-height"] && (i["max-height"] = 0), 0 == parseInt(i["max-height"]) ? (i["max-height"] = "0", setTimeout(function () {
i["max-height"] = n + "px"
}, 10)) : i["max-height"] = "0"
}, a = function (r) {
if (r.preventDefault(), !t) {
var a = this.closest("[data-gjs=navbar]"), o = a.querySelector("[data-gjs=navbar-items]");
i(o), e || (o.addEventListener(n, function () {
t = 0;
var e = o.style;
0 == parseInt(e["max-height"]) && (e.display = "", e["max-height"] = "")
}), e = 1)
}
};
"gjs-collapse" in this || this.addEventListener("click", a), this["gjs-collapse"] = 1
}.bind(items[i]))();
}</script>
{% endblock content %}
\ No newline at end of file
{% extends "base.jinja2" %}
{% block content %}
<div class="row" id="ir3o5">
<div id="icz0xd" class="cell">
<div id="igwru"><h1>Зарегистрированные в системе цели бэкапа и восстановления</h1></div>
</div>
</div>
<div class="row">
<div class="cell">
<table class="tapes">
<th>Уникальная метка</th>
<th>Состояние</th>
<th>Когда была в библиотеке</th>
<th>Где была в библиотеке (слот)</th>
<th>Местонахождение</th>
{% for tape in known_tapes %}
<tr>
<td>{{ tape['label'] }}</td>
<td>{{ tape['state'] }}</td>
<td>{{ tape['last_seen'] }}</td>
<td>{{ tape['last_seen_slot'] }}</td>
<td>{{ tape['location'] }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
{% endblock %}
\ No newline at end of file
{% extends "base.jinja2" %}
{% block content %}
<div class="row" id="ir3o5">
<div id="icz0xd" class="cell">
<div id="igwru"><h1>Зарегистрированные в системе цели бэкапа и восстановления</h1></div>
</div>
</div>
<div class="row" id="i1ejb">
<div class="cell" id="i0qb4">
<div id="iso3k">Цели бэкапа
</div>
<form class="form" id="backup_unique_label" action="/add_backup_target" method="post">
<div class="form-group">
<label class="label" id="ihwp5">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<input placeholder="Вбейте уникальный идентификатор цели бэкапа" class="input" id="i3f4c"/>
</div>
<div class="form-group" id="backup_local_fullpath">
<label class="label" id="ipsnl">Полный путь</label>
<input type="email" placeholder="Вбейте валидный и существующий локальный путь в ФС сервера backups" class="input"/>
</div>
<div class="form-group" id="backup_period_selector">
<label class="label" id="icpe8">Период сканирования</label>
<select class="select" id="idlsu"><option value="" id="itae1">- Выберите период -</option>
<option value="1">Раз в сутки</option>
<option value="2">Раз в двое суток</option>
<option value="7">Раз в неделю</option>
<option value="30">Раз в 30 дней</option>
</select>
</div>
<div class="form-group" id="i7o81">
<label class="label" id="i8ylf">Тип сканируемой цели (лишние файлы относительно типа игнорируются)</label>
<select class="select" id="i040c">
<option value="" id="i3twi">- Выберите тип -</option>
<option value="wgs">Полногеномные данные</option>
<option value="vcf">Данные VCF</option>
<option value="db">Данные баз данных</option>
<option value="files">Произвольные данные</option>
</select>
</div>
<div class="form-group" id="i0jbz">
<button type="submit" class="button" id="ir3p2">Зарегистрировать цель бэкапа</button>
</div>
</form>
<div class="row" id="icuox">
<div class="cell" id="itde6">
<table class="backup_targets">
<th>Уникальная метка</th>
<th>Тип</th>
<th>Период сканирования</th>
<th>Последнее сканирование</th>
<th>Полный путь</th>
<th>Число файлов</th>
<th>Размер (Гбайт)</th>
<th>Включена?</th>
{% for target in backup_targets %}
<tr>
<td>{{ target['unique_label'] }}</td>
<td>{{ target['kind'] }}</td>
<td>{{ target['rescan_interval'] }}</td>
<td>{{ target['last_scan_time'] }}</td>
<td>{{ target['fullpath'] }}</td>
<td>{{ target['files_count'] }}</td>
<td>{{ target['files_size_on_target_gb'] }}</td>
<td>{{ target['enabled'] }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
<div class="cell" id="iwxir">
<div id="iqvor">Цели восстановления
</div>
<form class="form" id="i5rah">
<div class="form-group">
<label class="label" id="isrn5">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<input placeholder="Type here your name" class="input"/>
</div>
<div class="form-group">
<label class="label" id="iodya">Полный путь</label>
<input type="email" placeholder="Type here your email" class="input"/>
</div>
<div class="form-group">
<button type="submit" class="button">Send</button>
</div>
</form>
<div class="row">
<div class="cell">
</div>
</div>
</div>
</div>
{% endblock %}
\ No newline at end of file
......@@ -5,6 +5,8 @@ from pyramid.view import view_config
from pyramid.response import Response
from pyramid.httpexceptions import HTTPException
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.types import BIGINT
from sqlalchemy.sql import func, and_
from .. import models
from .. import utils
......@@ -31,8 +33,18 @@ def add_backup_target(request):
@view_config(route_name='list_backup_targets', renderer='json', request_method='GET')
def list_backup_targets(request):
try:
targets = [x.to_dict() for x in request.dbsession.query(models.backuptarget.BackupTarget).all()]
return targets
targets = [x for x in request.dbsession.query(models.backuptarget.BackupTarget).all()]
target_to_return = []
for target in targets:
files_on_target = request.dbsession.query(models.file_to_backup.FileToBackup).filter(
target.unique_label == models.file_to_backup.FileToBackup.target_unique_label).count()
target_to_ret = target.to_dict()
target_to_ret['files_count'] = files_on_target
files_size_on_target = request.dbsession.query(func.sum(models.FileToBackup.fsize).cast(BIGINT)).filter(
target.unique_label == models.file_to_backup.FileToBackup.target_unique_label).scalar()
target_to_ret['files_size_on_target_gb'] = round(files_size_on_target / (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:
......
......@@ -3,7 +3,7 @@ from pyramid.view import view_config
from pyramid.response import Response
from pyramid.httpexceptions import HTTPException
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.sql import func
from sqlalchemy.sql import func, and_
from sqlalchemy.types import BIGINT
......@@ -14,26 +14,48 @@ from .. import models
def copy_status(request):
try:
backup_is_running = False
restore_is_running = False
for proc in psutil.process_iter():
try:
# Check if process name contains the given name string.
if 'python' in proc.name():
if any('backup_daemon.py' in x for x in proc.cmdline()):
backup_is_running = True
if any('restore_daemon.py' in x for x in proc.cmdline()):
restore_is_running = True
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
pass
total = request.dbsession.query(func.sum(models.FileToBackup.fsize).cast(BIGINT)).scalar()
copied = request.dbsession.query(func.sum(models.FileToBackup.fsize).cast(BIGINT)).filter(models.FileToBackup.tape_label != None).scalar()
total_files = request.dbsession.query(models.FileToBackup).filter(models.FileToBackup.is_file == True).count()
total_wgs = request.dbsession.query(func.sum(models.FileToBackup.fsize).cast(BIGINT)).filter(
and_(models.FileToBackup.is_file == True,
models.FileToBackup.kind == models.DataKind.wgs)).scalar()
copied = request.dbsession.query(func.sum(models.FileToBackup.fsize).cast(BIGINT)).filter(and_(models.FileToBackup.is_file == True,
models.FileToBackup.tape_label != None)).scalar()
copied_files = request.dbsession.query(models.FileToBackup).filter(and_(models.FileToBackup.is_file == True,
models.FileToBackup.tape_label != None)).count()
copied_wgs = request.dbsession.query(func.sum(models.FileToBackup.fsize).cast(BIGINT)).filter(
and_(models.FileToBackup.is_file == True,
models.FileToBackup.kind == models.DataKind.wgs,
models.FileToBackup.tape_label != None)).scalar()
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 {"backup_is_running": backup_is_running,
"restore_is_running": restore_is_running,
"total": total,
"total_gb": round(total / (1024**3), 2),
"total_files": total_files,
"total_wgs": total_wgs,
"total_wgs_gb": round(total_wgs / (1024**3), 2),
"copied": copied,
"copied_gb": round(copied / (1024**3), 2)}
"copied_gb": round(copied / (1024**3), 2),
"copied_wgs": copied_wgs,
"copied_wgs_gb": round(copied_wgs / (1024**3), 2),
"copied_files": copied_files}
# all batches list by time with status
@view_config(route_name='batches', renderer='json', request_method='GET')
......
from pyramid.view import view_config
from pyramid.response import Response
from pyramid.request import Request
from pyramid.httpexceptions import HTTPException
from .. import utils
@view_config(route_name='home', renderer='tapebackup:templates/home.jinja2')
def home_route(request):
return {}
try:
subreq = Request.blank('/copy_status')
copy_status = 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 {"copy_status": copy_status}
# this view is supposed to return current system status: library info and current state
@view_config(route_name='status', renderer='tapebackup:templates/status.jinja2')
def status_route(request):
try:
subreq = Request.blank('/copy_status')
copy_status = request.invoke_subrequest(subreq, use_tweens=True).json
manager = utils.tapemanager.TapeManager(request.dbsession)
library = manager.to_dict()
except HTTPException as e:
return Response(json_body={"error": e.detail}, content_type='application/json', status=e.status)
return library
\ No newline at end of file
return {"copy_status": copy_status, "library": library}
@view_config(route_name='targets', renderer='tapebackup:templates/targets.jinja2')
def targets_route(request):
try:
subreq = Request.blank('/list_backup_targets')
backup_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}
@view_config(route_name='tapes', renderer='tapebackup:templates/tapes.jinja2')
def tapes_route(request):
try:
subreq = Request.blank('/list_tapes')
tapes = 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 {"known_tapes": tapes}
\ No newline at end of file
from pyramid.view import view_config
from pyramid.response import Response
from pyramid.request import Request
from pyramid.httpexceptions import HTTPException, HTTPConflict, HTTPNotImplemented
from sqlalchemy.exc import SQLAlchemyError
from .. import models
# by status
def list_known_tapes():
return
\ No newline at end of file
@view_config(route_name='list_tapes', renderer='json')
def list_tapes(request):
try: