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

webui almost finished

parent e3a492e1
......@@ -84,7 +84,7 @@ qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = WARN
level = DEBUG
formatter = generic
[handler_filelog]
......
......@@ -4,6 +4,8 @@ def includeme(config):
config.add_route('status', '/status')
config.add_route('targets', '/targets')
config.add_route('tapes', '/tapes')
config.add_route('files', '/files')
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}')
......
......@@ -12,6 +12,10 @@ body {
flex-basis: 100%;
}
div:not(:last-child) {
margin-right: 40px;
}
.row {
display: flex;
justify-content: flex-start;
......@@ -75,7 +79,7 @@ body {
}
.paragraph {
padding: 10px;
padding: 0px;
}
table {
......@@ -91,7 +95,7 @@ table td {
.robot {
border: 1px solid black;
padding: 20px 5px;
padding: 10px 5px;
}
.gray {
......@@ -112,11 +116,11 @@ body {
text-align: center;
}
.paragraph {
padding: 10px;
.library {
font-size: x-small;
}
.library {
.files {
font-size: x-small;
}
......@@ -148,46 +152,50 @@ body {
}
.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;
.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
......@@ -13,7 +13,6 @@
<!-- Custom styles for this scaffold -->
<link href="{{request.static_url('tapebackup:static/css/style.css')}}" rel="stylesheet">
</head>
<body>
......
{% 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.size }}</td>
<td>{{ f.checksum }}</td>
<td>{{ f.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
......@@ -17,11 +17,11 @@
<li>из них wgs - {{ copy_status['copied_wgs_gb'] }} Гбайт</li>
<li>лабораторных номеров - TODO</li>
</ul>
<p class="paragraph">Кассет:</p>
<p class="paragraph">Кассет с данными:</p>
<ul>
<li>записано всего -  </li>
<li>вне ленточной библиотеки -</li>
<li>внутри ленточной библиотеки - </li>
<li>готовых к изъятию и перевозке - </li>
<li>записано всего - {{ tape_status['finalized_tapes'] }}</li>
<li>вне ленточной библиотеки - {{ tape_status['outside_library_tapes'] }}</li>
<li>внутри ленточной библиотеки - {{ tape_status['inside_library_tapes'] }}</li>
<li>готовых к изъятию и перевозке - {{ tape_status['ready_for_export_tapes'] }}</li>
</ul>
</div>
\ No newline at end of file
......@@ -6,204 +6,106 @@
</h1>
</section>
<div class="row">
{% include "stats.jinja2" %}
<div class="cell" id="icz0xd">
<div id="iifin7">Статус ленточной библиотеки</div>
<table id="HPE MSL3040" class="library">
<div style="flex-basis: 40%">
<h3 style="text-align: center">Краткая статистика</h3>
{% include "stats.jinja2" %}
<h3 style="text-align: center">Выгрузка кассет</h3>
<p><i>*чистящие кассеты не приводятся, т.к их замена обязана производиться из родного интерфейса библиотеки HPE с привязкой к разделу (единственному, но все же)</i></p>
<form class="form" id="export_tapes" action="/export_tapes" method="post">
<div class="form-group" id="tape_to_export">
<label class="label">Выберите кассету для перемещения в слот экспорта/импорта</label>
<select class="select">
<option value="">{% if ready_for_export %}- Выберите кассету для извлечения -{% else %}Нет доступных для выгрузки кассет{% endif %}</option>
{% for tape in ready_for_export %}
<option value="{{ tape.last_seen_slot }}">{{ tape.label }}, из слота {{ tape.last_seen_slot }}</option>
{% endfor %}
</select>
</div>
<div class="form-group" id="slot_to_export">
<label class="label">Выберите слот экспорта/импорта</label>
<select class="select">
<option value="">{% if empty_mailslot_slots %}- Выберите слот в мэйлслоте для выгрузки -{% else %}Все слоты в мэйлслоте заняты{% endif %}</option>
{% for slot in empty_mailslot_slots %}
<option value="{{ slot['element_address'] }}">{{ slot['element_address'] }}; пустой слот</option>
{% endfor %}
</select>
</div>
<div class="form-group" id="export_location">
<label class="label">Где будет храниться кассета после извлечения?</label>
<input placeholder="Вбейте адрес/описание места дальнейшего хранения" class="input"/>
</div>
<div class="form-group">
<button type="submit" class="button">Выгрузить кассету</button>
</div>
</form>
<h3 style="text-align: center">Загрузка кассет</h3>
<form class="form" id="export_tapes" action="/export_tapes" method="post">
<div class="form-group" id="slot_to_export">
<label class="label">Выберите слот экспорта/импорта, из которого забрать кассету в библиотеку</label>
<select class="select">
<option value="">{% if ready_for_import %}- Выберите слот с кассетой -{% else %}В мэйлслоте нет кассет{% endif %}</option>
{% for slot in ready_for_import %}
<option value="{{ slot['element_address'] }}">{{ slot['element_address'] }}; {{ slot['primary_volume_tag'] }}</option>
{% endfor %}
</select>
</div>
<div class="form-group">
<button type="submit" class="button">Загрузить кассету в библиотеку</button>
</div>
</form>
</div>
<div class="cell" style="flex-basis: 60%">
<div><h3 style="text-align: center">Статус ленточной библиотеки</h3></div>
<table id="HPE MSL3040" class="library" style="text-align: center">
<thead>
<tr>
<td id="empty_cell_left_upper">
</td>
<td id="drive">
<p class="paragraph" id="izj3r">Основной tape-drive
<br/>{% if library['drives'][0]['full'] %}{{ library['drives'][0]['primary_volume_tag'] }}{% else %}Пустой{% endif %}
<p class="paragraph">Основной tape-drive
<br/>{% if library['drives'][0]['full'] %}Штрихкод кассеты: <b>{{ library['drives'][0]['primary_volume_tag'] }}</b>{% else %}Пустой{% endif %}
<br/>{% if library['drives'][0]['full'] and library['df'] != 0 %}
Кассета смонтирована; {{ library['df'] / 1024**3 }} Гбайт свободно{% endif %}
Кассета смонтирована;<br/><b>{{ (library['df'] / 1024**3) | round(2) }}</b> Гбайт свободно{% endif %}
</p></td>
<td id="empty_cell_right_upper">
</td>
</tr>
</thead>
<tbody id="i4ska">
<tbody>
<tr>
<td></td>
<td rowspan="21" id="ip0wy">
<div class="robot"><p class="paragraph">
robot
<br/>12CHAR
<br/>Status
</p></div>
<td rowspan="21">
<div class="robot">
<p class="paragraph">
Робот: {% if library['robot']['full'] %}Штрихкод кассеты: <b>{{ library['robot']['primary_volume_tag'] }}</b>{% else %}пустой{% endif %}
</p>
</div>
</td>
<td></td>
</tr>
<!-- TODO: continue here! need to make zip-like "by-slot" library item -->
<tr>
<td><p class="paragraph" id="ismwm">
l01
<br/>12CHAR
<br/>Status
</p></td>
<td><p class="paragraph">
r01
<br/>12CHAR
<br/>Status
</p></td>
</tr>
<tr>
<td><p class="paragraph" id="i6lmi">
l02
</p></td>
<td><p class="paragraph">
r02
</p></td>
</tr>
<tr>
<td><p class="paragraph" id="ie564">
l03
</p></td>
<td><p class="paragraph">
r03
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l04
</p></td>
<td><p class="paragraph" id="ijngj">
r04
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l05
</p></td>
<td id="icfni"><p class="paragraph">
r05
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l06
</p></td>
<td><p class="paragraph">
r06
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l07
</p></td>
<td id="im9mw"><p class="paragraph">
r07
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l08
</p></td>
<td><p class="paragraph">
r08
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l09
</p></td>
<td><p class="paragraph">
r09
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l10
</p></td>
<td><p class="paragraph">
r10
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l11
</p></td>
<td><p class="paragraph">
r11
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l12
</p></td>
<td><p class="paragraph">
r12
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l13
</p></td>
<td><p class="paragraph">
r13
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l14
</p></td>
<td><p class="paragraph">
r14
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l15
</p></td>
<td><p class="paragraph">
r15
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l16
</p></td>
<td class="gray"><p class="paragraph">
r16
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l17
</p></td>
<td class="gray"><p class="paragraph">
r17
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l18
</p></td>
<td class="gray"><p class="paragraph">
r18
</p></td>
</tr>
<tr>
<td><p class="paragraph">
l19
</p></td>
<td class="gray"><p class="paragraph">
r19
</p></td>
</tr>
<!-- NOTE: this is hardcoded for specific library model and configuration -->
{% for i in range(20) %}
<tr>
<td><p class="paragraph">
l20
Номер слота: <b>{{ table_view[i]['element_address'] }}</b>
<br/>Штрихкод кассеты: <b>{{ table_view[i]['label'] }}</b>
<br/>Состояние: <b>{{ table_view[i]['state'] }}</b>
</p></td>
<td id="i7c971" class="gray"><p class="paragraph">
r20
<td {% if table_view[i + 20]['element_address'] < 1000 %}class="gray"{% endif %}><p class="paragraph">
Номер слота: <b>{{ table_view[i + 20]['element_address'] }}</b>
<br/>Штрихкод кассеты: <b>{{ table_view[i + 20]['label'] }}</b>
<br/>Состояние: <b>{{ table_view[i + 20]['state'] }}</b>
{% if table_view[i + 20]['element_address'] < 1000 %}
<br/><i>секция для импорта-экспорта<br/>кассет (мэйлслот)</i>
{% endif %}
</p></td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
......
{% extends "base.jinja2" %}
{% block content %}
<div class="row" id="ir3o5">
<div id="icz0xd" class="cell">
<div id="igwru"><h1>Зарегистрированные в системе цели бэкапа и восстановления</h1></div>
<div class="row">
<div class="cell">
<div"><h1>Зарегистрированные в системе цели бэкапа и восстановления</h1></div>
</div>
</div>
<div class="row" id="i1ejb">
<div class="cell" id="i0qb4">
<div id="iso3k">Цели бэкапа
<div class="row">
<div class="cell" style="flex-basis: 60%">
<div>Цели бэкапа
</div>
<form class="form" id="backup_unique_label" action="/add_backup_target" method="post">
<div class="form-group">
<label class="label" id="ihwp5">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<label class="label">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<input placeholder="Вбейте уникальный идентификатор цели бэкапа" class="input" id="i3f4c"/>
</div>
<div class="form-group" id="backup_local_fullpath">
<label class="label" id="ipsnl">Полный путь</label>
<label class="label">Полный путь</label>
<input type="email" placeholder="Вбейте валидный и существующий локальный путь в ФС сервера backups" class="input"/>
</div>
<div class="form-group" id="backup_period_selector">
......@@ -69,20 +69,20 @@
</div>
</div>
</div>
<div class="cell" id="iwxir">
<div class="cell" id="iwxir" style="flex-basis: 40%">
<div id="iqvor">Цели восстановления
</div>
<form class="form" id="i5rah">
<form class="form">
<div class="form-group">
<label class="label" id="isrn5">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<input placeholder="Type here your name" class="input"/>
<label class="label">Уникальная метка (только буквы, цифры и подчеркивания)</label>
<input placeholder="Дайте уникальное имя цели восстановления" class="input"/>
</div>
<div class="form-group">
<label class="label" id="iodya">Полный путь</label>
<input type="email" placeholder="Type here your email" class="input"/>
<label class="label">Полный путь к точке монтирования цели восстановления</label>
<input placeholder="Точка монтирования" class="input"/>
</div>
<div class="form-group">
<button type="submit" class="button">Send</button>
<button type="submit" class="button">Зарегистрировать цель восстановления</button>
</div>
</form>
<div class="row">
......
......@@ -4,15 +4,40 @@ from pyramid.request import Request
from pyramid.httpexceptions import HTTPException
from .. import utils
from .. import models
from sqlalchemy.sql import and_
import logging
log = logging.getLogger(__name__)
def tape_stats(request):
finalized_tapes = request.dbsession.query(models.Tape).filter(
models.Tape.state == models.TapeState.finalized).count()
inside_library_tapes = request.dbsession.query(models.Tape).filter(
models.Tape.location == "Inside tape library").count()
outside_library_tapes = request.dbsession.query(models.Tape).filter(
models.Tape.location != "Inside tape library").count()
ready_for_export_tapes = request.dbsession.query(models.Tape).filter(
and_(models.Tape.location == "Inside tape library",
models.Tape.state == models.TapeState.finalized)).count()
tape_status = {"finalized_tapes": finalized_tapes,
"inside_library_tapes": inside_library_tapes,
"outside_library_tapes": outside_library_tapes,
"ready_for_export_tapes": ready_for_export_tapes}
return tape_status
@view_config(route_name='home', renderer='tapebackup:templates/home.jinja2')
def home_route(request):
try:
subreq = Request.blank('/copy_status')
copy_status = request.invoke_subrequest(subreq, use_tweens=True).json
tape_status = tape_stats(request)
except HTTPException as e:
return Response(json_body={"error": e.detail}, content_type='application/json', status=e.status)