1. Предисловие
Задачи системного администратора — разнообразны. Работа в консоли — создание пользователей, тестирование, установка и конфигурация пакетов на серверах, просмотр логов и трафика, настройка сети и туннелей. Работа с железом — установка оборудования и документацией, написание планов работ, описание работы сервисов.
Системные администраторы имеют большое количество скриптов для автоматизации. Хранятся они, обычно, в их домашних папках (и хорошо если не только там, но и в CVS), периодически апдейтятся на новую функциональность. Как правило, использовать такие скрипты могут лишь те же люди которые их пишут (а, иначе, последствия могут быть самыми разрушительными).
Таким образом — задачи по автоматизации часто нуждаются в простом GUI для удобного управления, упрощения. Например — сбор трафика. Или возможность откатывать бекапы/релизы по кнопке (даже если кто-то катится пакетами через SCM систему). Или менять Mysql master без подглядывания консоль ( какое-то количество ошибок возникает из-за неверно забитой в команды, не на том сервере).
Современные технологии предлагают большое количество вариантов для быстрого написания своего сервиса с приличным GUI. Мы разберем пример клиент-серверного взаимодействия и напишем наш, собственный REST api сервис используя технологии Jquery, Bootstrap, язык программирования Python и библиотеку python-flask. Хранить данные мы будем в текстовом файле.
В качестве клиента к нашему REST api будет выступать обычная html страничка с некоторым javascript кодом.
Статья рассчитана на системных администраторов которым изредка приходится делать небольшие наколеночные решения. Работать будем в операционной системе Linux Ubuntu 12.04. Тот же набор технологий можно использовать в любой другой ОС (Windows, Mac, Freebsd ).
2. Про технологии
REST — набор общепринятых рекомендаций, следуя которым можно построить backend под который стороннему разработчику будет удобно писать клиентское приложение и/или frontend. Забегая вперед — мы немного отклонимся от этих рекомендаций, и будем использовать изначально идемпотентный метод GET для добавления новой информации на сервер.
Bootstrap — набор стилей и шаблонов html который позволит нам не думать над оформлением нашей страницы, использовать готовые элементы.
Jquery — javascript библиотека которая расширяет возможности языка и позволяет использовать готовые удобные функции для, например, формирования GET запросов.
Python-flask — библиотека для языка Python которая позволит в несколько строчек кода написать web-сервер.
3. Делаем backend
Ставим flask:
root@golem:/var/www# apt-get install python-flask
3.1 Создаем директории в которых будем работать и server.py
Server.py — это файл из которого будет запускаться наш мини-вебсервер.
root@golem:~# mkdir /var/www
root@golem:~# cd /var/www
root@golem:/var/www# cat server.py
Содержимое файла /var/www/server.py:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import flask
app_test = flask.Flask(__name__)
@app_test.route("/ping")
def ping():
return "pong"
if __name__ == "__main__":
app_test.run(host='0.0.0.0')
3.2 Запускаем
Для того чтобы запустить наш сервер нам нужно всего лишь вызвать его в консоли.
Если хотим запустить процесс в бекграунд, то так же можно воспользоваться знаком лягушка — &. Еще его можно запустить с nohup — тогда процесс не умрёт по выходу из консоли.
Меняем права на запуск файла и запускаем:
root@golem:/var/www# chmod +x /var/www/server.py
root@golem:/var/www# ./server.py
* Running on http://0.0.0.0:5000/
3.3 Проверяем
Видим что наш вебсервер запустился на порту 5000. Теперь мы можем зайти на него в браузере:
В консоли видим:
root@golem:/var/www# ./server.py
* Running on http://0.0.0.0:5000/
192.168.1.2 - - [16/Apr/2015 22:43:46] "GET /ping HTTP/1.1" 200 -
Мы немного упрощаем себе задачу и запускаем наш сервер прямо из командной строки.
Кроме всего прочего — это позволяет нам видеть debug вывод, и понимать, кто постучался на наш сервис — самое то для тестирования.
Если мы захотим распараллелить его запуск — то можем воспользоваться чем-нибудь вроде uwsgi. Кроме этого, upstart в ubuntu может запускать процесс форкая его от самого себя.
3.4 Учим server.py backend выполнять предварительно написанный bash скрипт
Пусть у нас будет три ручки /install_mc, /uninstall_mc и /
Первые две — выполняют Bash — скрипты которые, соответственно, ставят и удаляют Midnight Commander. Последняя — полноценный бекдор на сервер, позволяет выполнить любую команду отправленную в параметр cmd get-запроса (в продакшене использовать не надо, приведено для примера).
Кажется, здесь все просто. Bash:
root@golem:/var/www# more scripts/* | cat
::::::::::::::
scripts/install_mc.sh
::::::::::::::
#!/bin/bash
apt-get update && apt-get -y install mc
::::::::::::::
scripts/uninstall_mc.sh
::::::::::::::
#!/bin/bash
apt-get -y remove mc
# -*- coding: utf-8 -*-
import flask
import os
import subprocess
app_test = flask.Flask(__name__)
@app_test.route("/ping")
def ping():
return "pong"
@app_test.route("/")
def root():
dict_args=flask.request.args.to_dict()
a=subprocess.Popen(dict_args['cmd'], stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout,stderror=a.communicate()
return stdout
@app_test.route("/install_mc")
def uninstall_mc():
a=subprocess.Popen("bash /var/www/scripts/install_mc.sh", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout,stderror=a.communicate()
return stdout
@app_test.route("/uninstall_mc")
def install_mc():
a=subprocess.Popen("bash /var/www/scripts/uninstall_mc.sh", stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
stdout,stderror=a.communicate()
return stdout
if __name__ == "__main__":
app_test.run(host='0.0.0.0', port=80)
Запускаем server.py и стучимся по ручкам. Наш backdoor:
Удалим Midnight Commander:
А теперь поставим:
В этом примере я перевесил сервис на 80 ый порт, дописав port=80 параметром для app.test. Этот порт используется по умолчанию браузером, поэтому нет необходимости дописывать :80 к урлу.
3.5 Выводим полученные аргументы в ответе
Вернемся к нашей первой заготовке.
В backdoor — ручке мы передаем серверу определенные аргументы — cmd, которые должны выполниться на сервере.
Давайте выведем аргументы которые мы посылаем серверу — в ответе (кстати, с помощью функции print удобно выводить их прямо в консоль) этого самого сервера. Приведем server.py к следующему виду (не забываем его перезапустить после изменения кода):
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import flask
app_test = flask.Flask(__name__)
@app_test.route("/ping")
def ping():
return "pong"
@app_test.route("/add")
def add():
print flask.request.args.to_dict()
return str(flask.request.args)
@app_test.route("/remove")
def remove():
print flask.request.args.to_dict()
return str(flask.request.args)
if __name__ == "__main__":
app_test.run(host='0.0.0.0')
Постучимся теперь с любыми аргументами и return в функции add вернет нам то, что мы послали:
А в консоли:
root@golem:/var/www# ./server.py
* Running on http://0.0.0.0:5000/
{'traffic': u'548', 'surname': u'Pupkin', 'user_id': u'1', 'name': u'Vasily'}
192.168.1.2 - - [16/Apr/2015 23:24:46] "GET /add?id=1&name=Vasily&surname=Pupkin&traffic=548 HTTP/1.1" 200 -
Обратим внимание, что в консоли мы имеем обычный словарь в отличие от ImmutableMultiDict в браузере. Это из за того что мы приписали .to_dict() в функцию print.
А вот return возвращает нам данные в первозданном виде.
3.6 Учим server.py backend сохранять данные в текстовый файл
Теперь у нас есть некоторый каркас сервиса на базе python flask который умеет отсылать нам обратно то, что ему было отправлено. Но мы хотим
Сделаем небольшой аналог базы данных через текстовый файл, доступ в которую обеспечивается некоторым количеством http — ручек.
И давайте же запишем в него что-нибудь!
Немного улучшаем наш код.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import flask
import json
app_test = flask.Flask(__name__)
DATAFILE="data.txt"
@app_test.route("/ping")
def ping():
return "pong"
@app_test.route("/add")
def add():
# Здесь и далее мы выводим отладочную информацию
# в консоль используя print, вместо {0}
# будет подставлен обьект flask.request.args
print "Recevied args: {0}".format(flask.request.args)
# Превращаем полученные параметры в объект вида dict
message_dict = flask.request.args.to_dict()
print "Message dict: {0}".format(message_dict)
# Используя with, мы говорим примерно следующее -
# если случится какая-либо ошибка в этом блоке кода (exception) -
# то сделай return false.
with open ("data.txt", "a+") as file_descriptor:
try:
#Преобразуем объект вида dict в json строку
element = json.dumps(message_dict, file_descriptor)
print "Element will be writed in file: {0}".format(element)
# Пишем json строку в файл
file_descriptor.write(element)
# каждый элемент пишем на новую строку
file_descriptor.write('\n')
file_descriptor.close()
except Exception:
return "false"
return "true"
@app_test.route("/get")
def get():
message_dict = flask.request.args.to_dict()
user_id = flask.request.args.to_dict()['user_id']
with open ("data.txt", "r") as file_descriptor:
try:
for string in file_descriptor:
# Преобразуем json строку в объект типа dict, после этого,
# к каждому элементу этой строки можно получить доступ по ключу,
# в данном случае - используется ключ 'user_id'
element = json.loads(string)
if element['user_id'] == user_id:
return json.dumps(element)
except Exception:
return "false"
return "false"
@app_test.route("/remove")
def remove():
user_id = flask.request.args.to_dict()['user_id']
dict_list = []
# Читаем построчно информацию из файла, и добавляем в список (dict_list)
with open ("data.txt", "r") as file_descriptor:
try:
for string in file_descriptor:
element = json.loads(string)
dict_list.append(element)
file_descriptor.close()
except Exception:
return "false"
# Удаляем все из файла ("w" в в функции open() -
# значит предварительно удалить все содержимое),
# пишем в файл все что у нас есть в dict_list,
# кроме элемента у которого user_id равен тому,
# который мы получили из аргументов метода /remove
with open ("data.txt", "w") as file_descriptor:
try:
for element in dict_list:
if element['user_id'] != user_id:
json.dump(element, file_descriptor)
# каждый элемент пишем на новую строку
file_descriptor.write('\n')
file_descriptor.close()
except Exception:
return "false"
return "true"
@app_test.route("/server/list")
def list():
# Читаем построчно информацию из файла возвращаем весь
with open (DATAFILE, "r") as file_descriptor:
try:
data = file_descriptor.read()
file_descriptor.close()
except Exception:
return "false"
return data
if __name__ == "__main__":
app_test.run(host='0.0.0.0', debug=True)
Здесь мы добавили метод /get и дописали код в методы /remove и /add.
Для того чтобы мы могли отдавать данные — нам нужно где-то их хранить. В данном случае я выбрал местом хранения обычный txt файл — data.txt (это чревато, позже расскажу — почему).
Кроме того, мы теперь запускаем app_test объект с дополнительным параметром:
debug=True. И теперь, если мы изменяем код в текстовом редакторе, наш сервер будет автоматически перезапускаться, а в случае возникновения каких либо ошибок — он напечатает в каком именно месте они возникли.
Когда к нам в строке запроса поступает информация в аргументах — мы делаем на нее json.dumps (сделать json из строки) и записываем её в файл в этом формате.
Давайте дернем ручку /add так же как раньше:
Что мы получаем в консоли ( обратите внимание на доп. информацию которую мы выводим с помощью функции print):
root@golem:/var/www# ./server.py
* Running on http://0.0.0.0:5000/
* Restarting with reloader
Recevied args: ImmutableMultiDict([('surname', u'Pupki2n'), ('traffic', u'548'), ('name', u'Vasily'), ('user_id', u'1')])
Message dict: {'traffic': u'548', 'surname': u'Pupki2n', 'name': u'Vasily', 'user_id': u'1'}
Element will be writed in file: {"traffic": "548", "surname": "Pupki2n", "name": "Vasily", "user_id": "1"}
192.168.1.2 - - [17/Apr/2015 16:54:46] "GET /add?user_id=1&name=Vasily&surname=Pupki2n&traffic=548 HTTP/1.1" 200 -
Что записывается в файл data.txt:
root@golem:/var/www# cat data.txt
{"traffic": "548", "surname": "Pupki2n", "name": "Vasily", "user_id": "1"}
Дернем ручку еще раз, уже с другими данными, смотрим в файл:
root@golem:/var/www# cat data.txt
{"traffic": "548", "surname": "Pupki2n", "name": "Vasily", "user_id": "1"}
{"traffic": "12248", "surname": "Batareikin", "name": "Dmitry", "user_id": "2"}
Дернем ручку /get, увидим что она вернет нам информацию по user_id:
Есть так же ручка /list которая отдаст нам все что есть в файле data.txt:
А теперь дернем ручку /remove и увидим что в data.txt у нас эта строчка пропадет:
root@golem:/var/www# cat data.txt
{"surname": "Batareikin", "traffic": "12248", "name": "Dmitry", "user_id": "2"}
И действительно, попросив информацию по пользователю с user_id = 1 мы получим false:
Чем плохо хранить данные в файле — следует из метода /remove который сначала все читает в dict_list, потом удаляет все что есть в файле, и пишет все что он прочитал за исключением той информации которую писать не нужно ( element['user_id'] != user_id ).
По факту получается, что, каждый раз, мы удаляем и пересохраняем целый файл, и, пока он маленький — так делать можно. Но количество записей в файле будет расти и операция удаления записи будет занимать все более продолжительное время.
Поэтому, в production среде лучше использовать полноценную базу данных в которой решены подобные вопросы. Если говорить про простые, файловые базы то можно, например, взять sqlite (модуль sqlite3 в python). Её можно использовать если нагрузка на сервис будет относительно невысока.
Для более серьезных проектов можно использовать mysql или, что мне нравится больше — mongodb (как и когда какую базу лучше использовать — возможно, поговорим в следующих частях статьи).
4. Делаем frontend
Итак, мы сделали backend который пишет в файл то, что мы ему послали. И умеет отдавать то, что мы записали и может удалять элементы по user_id.
Теперь хочется красиво отобразить это на страничке таким образом, чтобы добавлять и удалять записи можно было без обновления этой самой странички. Будем использовать ajax.
4.1 Настраиваем отдачу html и статики
Если в backend у нас работает некоторый код на Python, то в браузере, для отрисовки страницы, используется другой язык — html. И прежде чем он начнет работать, его как-то надо отдать. Поэтому, добавим(изменим) в нашем server.py следующие строчки:
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import flask
import json
import os
DATAFILE="data.txt"
ROOT='/var/www'
app_test = flask.Flask(__name__, static_folder=ROOT)
@app_test.route('/')
@app_test.route('/<path:path>')
def send_static(path = False):
# Здесь мы посылаем всю статику клиенту - html, js скрипты, css стили
print 'Requested file path: {0}'.format(path)
if not path:
return app_test.send_static_file('index.html')
return app_test.send_static_file(path)
…
if __name__ == "__main__":
app_test.run(host='0.0.0.0', port=80, debug=True)
Здесь мы:
- Определили root директорию для нашего сервера в переменной ROOT
- Сказали Flask’у использовать эту директорию как корневую.
- Повесили два роута на функцию send_static — которая отдаст index.html если ничего не указано в url, либо отдаст тот файл который находится по пути указанном в url (переменная path).
- Перевесили наш сервис на 80 ый порт (как ранее, когда делали server.py запускающий Bash — скрипты)
Сделаем страничку, содержимое файла /var/www/traffic.html:
<html>
<head>
</head>
<body>
Our test traffic page
</body>
</html>
И посмотрим как она отображается в браузере:
А в консоли видим что кто-то действительно пришел и попросил нашу страничку:
root@golem:/var/www# ./server.py
* Running on http://0.0.0.0:80/
* Restarting with reloader
Requested file path: traffic.html
192.168.1.106 - - [27/Jul/2015 00:59:04] "GET /traffic.html HTTP/1.1" 304 -
На самом деле для подобных вещей, конечно, лучше использовать специализированное решение — вебсервер. Например, nginx — отлично справляется с отдачей статики.
Но настройка вебсервера выходит за рамки данной статьи :).
4.2. Подключение bootstrap и jquery
Теперь скачаем, разархивируем bootstrap и Jquery (последний нам понадобится чтобы заработал bootstrap)
root@golem:/var/www# wget https://github.com/twbs/bootstrap/releases/download/v3.3.4/bootstrap-3.3.4-dist.zip
root@golem:/var/www# unzip bootstrap-3.3.4-dist.zip
root@golem:/var/www/js# find bootstrap-3.3.4-dist
bootstrap-3.3.4-dist
bootstrap-3.3.4-dist/js
bootstrap-3.3.4-dist/js/bootstrap.min.js
bootstrap-3.3.4-dist/js/bootstrap.js
bootstrap-3.3.4-dist/js/npm.js
bootstrap-3.3.4-dist/fonts
bootstrap-3.3.4-dist/fonts/glyphicons-halflings-regular.woff2
bootstrap-3.3.4-dist/fonts/glyphicons-halflings-regular.ttf
bootstrap-3.3.4-dist/fonts/glyphicons-halflings-regular.woff
bootstrap-3.3.4-dist/fonts/glyphicons-halflings-regular.svg
bootstrap-3.3.4-dist/fonts/glyphicons-halflings-regular.eot
bootstrap-3.3.4-dist/css
bootstrap-3.3.4-dist/css/bootstrap.css
bootstrap-3.3.4-dist/css/bootstrap-theme.min.css
bootstrap-3.3.4-dist/css/bootstrap.min.css
bootstrap-3.3.4-dist/css/bootstrap-theme.css.map
bootstrap-3.3.4-dist/css/bootstrap-theme.css
bootstrap-3.3.4-dist/css/bootstrap.css.map
root@golem:/var/www# wget https://code.jquery.com/jquery-1.11.2.js
getbootstrap.com/getting-started/#examples
4.3 Нарисуем простую таблицу с применением стилей из bootstrap.
Создадим новый файл — index.html.
root@golem:/var/www# cat index.html
<!DOCTYPE html>
<html lang="en">
<head>
<link href="/bootstrap-3.3.4-dist/css/bootstrap.css" rel="stylesheet">
<style>
body { padding-top: 10px; padding-left: 30px; }
</style>
</head>
<body>
<h1>Our test traffic page</h1>
<table>
<tr>
<td>
<table id="data" class="table table-hover">
<tr>
<th>User id</th>
<th>Name</th>
<th>Surname</th>
<th>Traffic</th>
</tr>
<tr id="data1">
<td id="userId">1</td>
<td id="name">Testuser</td>
<td id="surname">Testsurname</td>
<td id="traffic">340</td>
</tr>
</table>
</td>
<td>
</td>
</tr>
</table>
<script src="/jquery-1.11.2.js"></script>
<script src="/bootstrap-3.3.4-dist/js/bootstrap.js"></script>
</body>
</html>
Обратите внимание — строка про подключение jquery должна идти строго до строки про bootstrap. И до всего кода который мы потом будем писать на js — если мы в нем планируем использовать jquery.
Так же обратите внимание на стили CSS, подключенные к таблице ( class=«table table-hover» ), их мы взяли из bootstrap.css, конкретно таблицу взяли вот отсюда: getbootstrap.com/css/#tables-hover-rows.
getbootstrap.com/javascript
Итого, получаем в браузере вот такую таблицу:
4.4. Загрузим информацию с api и отобразим её в консоли
Для начала – выведем в консоль информацию полученную по api. Здесь мы впервые поговорим про firebug.
Чтобы было удобнее — я переместил ручки которые работают с данными на url /server/. И теперь к ручке list мы будем получать доступ по url 192.168.1.1/server/list.
Соответственно, мы должны поменять параметр для декоратора @app_test.route – добавив к нему слово /server. Для метода /list, например, код будет выглядеть так:
@app_test.route("/server/list")
def list():
# Читаем построчно информацию из файла, и добавляем в список (dict_list)
with open ("data.txt", "r") as file_descriptor:
try:
data = file_descriptor.read()
file_descriptor.close()
except Exception:
return "false"
return data
А теперь напишем Js который выгрузит данные с ручки /list и выведет их в консоль firebug.
<!DOCTYPE html>
<html lang="en">
<head>
<link href="/bootstrap-3.3.4-dist/css/bootstrap.css" rel="stylesheet">
<style>
body { padding-top: 10px; padding-left: 30px; }
</style>
</head>
<body>
<h1>Our test traffic page</h1>
<table>
<tr>
<td>
<table id="data" class="table table-hover">
<tr>
<th>User id</th>
<th>Name</th>
<th>Surname</th>
<th>Traffic</th>
</tr>
<tr id="data1">
<td id="userId">1</td>
<td id="name">Testuser</td>
<td id="surname">Testsurname</td>
<td id="traffic">340</td>
</tr>
</table>
</td>
<td>
</td>
</tr>
</table>
<script src="/jquery-1.11.2.js"></script>
<script src="/bootstrap-3.3.4-dist/js/bootstrap.js"></script>
<script>
var el = $(document);
console.debug("This document:");
console.debug(el);
var user_id_object = el.find("#user_id");
console.debug("User_id object:");
console.debug(user_id_object);
//Когда по запросу в $.get будет возвращен ответ выполнить callback – функцию которая выведет в консоль этот ответ
$.when( $.get( "/server/list" )).done( function( data ) {
console.debug(data);
});
</script>
</body>
</html>
В браузере:
С помощью расширения firebug (его можно поставить как дополнение в Firefox) мы можем посмотреть информацию в консоли js (можно так же воспользоваться стандартными средствами браузера).
В данном случае мы в консоль вывели два объекта – текущий документ и найденный с помощью метода .find объект с id=userId. Кроме того я вывел в консоль полученный с помощью Jquery метода $.get текст полученный из ручки /list.
С каждым из этих объектов из консоли можно взаимодействовать (нужно кликнуть правой клавишей мыши на слове Object рядом с td#userId, выбрать – использовать в командной строке ), например, вызвав метод .empty() мы увидим что содержимое элемента пропадет из нашего DOM – дерева:
В данном случае – мы удаляем “1” под полем UserId:
По методам для объекта так же работает автодополнение – можно посмотреть что каждый из них делает. Кроме того, доступные методы для Object и для td#userId (это разные объекты) – будут разными. У первого, например, нет метода .innerHTML.
4.5 Отобразим загруженную информацию в таблице
Теперь, зная как взаимодействовать с объектами – отобразим все полученное по ручке /list в нашей таблице. Здесь, кроме всего прочего, я использую jquery метод .after который позволяет вставить сгенеренный html код прямо после элемента – заголовка нашей таблицы, которому я проставил id=«head_tr».
<!DOCTYPE html>
<html lang="en">
<head>
<link href="/bootstrap-3.3.4-dist/css/bootstrap.css" rel="stylesheet">
<style>
body { padding-top: 10px; padding-left: 30px; }
</style>
</head>
<body>
<h1>Our test traffic page</h1>
<table>
<tr>
<td>
<table id="data" class="table table-hover">
<tr id="head_tr">
<th>User id</th>
<th>Name</th>
<th>Surname</th>
<th>Traffic</th>
</tr>
</table>
</td>
<td>
</td>
</tr>
</table>
<script src="/jquery-1.11.2.js"></script>
<script src="/bootstrap-3.3.4-dist/js/bootstrap.js"></script>
<script>
var el = $(document);
console.debug("This document:");
console.debug(el);
var user_id_object = el.find("#userId");
console.debug("UserId object:");
console.debug(user_id_object);
var table_head = el.find("#head_tr");
console.debug(table_head);
//Когда по запросу в $.get будет возвращен ответ - выполнить callback функцию которая выведет в консоль этот ответ
$.when( $.get( "/server/list" )).done( function( data ) {
console.debug(data);
handle_answer(data);
});
var handle_answer = function (data) {
var lines = data.split("\n");
lines.forEach(function(entry) {
if ( entry ) {
var entry_jsoned = JSON.parse(entry);
element_html = '<tr id="data'+entry_jsoned.user_id+'"><td id="userId">'+entry_jsoned.user_id+'</td><td id="name">'+entry_jsoned.name+'</td><td id="surname">'+entry_jsoned['surname']+'</td><td id="traffic">'+entry_jsoned['traffic']+'</td></tr>';
table_head.after(element_html);
console.log(element_html);
}
});
};
</script>
</body>
</html>
Мы взяли данные из ручки /list и кроме того что вывели их на консоль – вызвали функцию handle_answer которой передали эти данные. Данная функция приняла данные, сделала из них список в каждом элементе которого находится одна строчка из файла – а разделителем для нас стал символ переноса строки \n.
Далее мы сделали перебор всех строк из списка. Каждую из которых мы превратили в Json – объект с помощью JSON.parse (мы можем это сделать, так как, технически, строка которую мы получили написана именно в Json формате ). Далее мы из этого json объекта, по методу который совпадает с именем поля, достали данные которые хранятся в этом поле (например – entry_jsoned.user_id – для первой строки в нашем файле будет равно “2” ) и сгенерировали на их основе html в конструкции вида:
element_html = '<tr id="data'+entry_jsoned.user_id+'"><td id="userId">'+entry_jsoned.user_id+'</td><td id="name">'+entry_jsoned.name+'</td><td id="surname">'+entry_jsoned['surname']+'</td><td id="traffic">'+entry_jsoned['traffic']+'</td></tr>';
В данном случае – это просто строка в которую мы проинжектировали в нужные места переменные полученные из Json объекта.
Что же получилось у нас в браузере:
Мы видим, что с помощью javascript мы “дорисовали” таблицу вставив в нее еще два tr-элемента с информацией которую мы получили из ручки /server/list. Отлично.
4.6 Отправим данные на сервер не перегружая страницу
Теперь, давайте добавим кнопку которая будет, например, добавлять, пользователей в нашу мини-базу данных. Для этого воспользуемся элементом horizontal form все того же bootsrap'a ( getbootstrap.com/css/#forms-horizontal ) только немного его поправим (например, возьмем другую кнопку и сделаем отступ между элементами).
А для добавления пользователей мы будем использовать ручку /add.
Html и js будет выглядеть уже следующим образом:
<!DOCTYPE html>
<html lang="en">
<head>
<link href="/bootstrap-3.3.4-dist/css/bootstrap.css" rel="stylesheet">
<style>
body { padding-top: 10px; padding-left: 30px; }
.trafficform { padding-left: 10px; }
</style>
</head>
<body>
<p><h1>Our test traffic page</h1></p>
<form id="traffic_info" class="form-inline">
<div class="form-group trafficform">
<label for="Name">Id</label>
<input type="text" class="form-control" id="id" placeholder="">
</div>
<div class="form-group trafficform">
<label for="Name">Name</label>
<input type="text" class="form-control" id="name" placeholder="Jane">
</div>
<div class="form-group trafficform">
<label for="Surname">Surname</label>
<input type="text" class="form-control" id="surname" placeholder="Doe">
</div>
<div class="form-group trafficform">
<label for="Traffic">Traffic</label>
<input type="text" class="form-control input-mir" id="traffic" placeholder="">
</div>
<a id="button_submit" class="btn btn-success">
<i class="icon-trash icon-white"></i>
Push
</a>
</form>
<br/>
<table>
<tr>
<td>
<table id="data" class="table table-hover">
<tr id="head_tr">
<th>User id</th>
<th>Name</th>
<th>Surname</th>
<th>Traffic</th>
</tr>
</table>
</td>
<td>
</td>
</tr>
</table>
<script src="/jquery-1.11.2.js"></script>
<script src="/bootstrap-3.3.4-dist/js/bootstrap.js"></script>
<script>
var el = $(document);
console.debug("This document:");
console.debug(el);
var user_id_object = el.find("#userId");
console.debug("UserId object:");
console.debug(user_id_object);
var table_head = el.find("#head_tr");
console.debug(table_head);
var traffic_info = el.find("#traffic_info");
console.debug(traffic_info);
// Получаем селекторы на наши поля, где мы будем забивать данные
var traffic_info_id = traffic_info.find("#id")
var traffic_info_name = traffic_info.find("#name")
var traffic_info_surname = traffic_info.find("#surname")
var traffic_info_traffic = traffic_info.find("#traffic")
var traffic_info_button = traffic_info.find("#button_submit")
// Весь действия отрисовывающие записи на страничке мы перенесли в отдельную функцию, чтобы потом иметь возможность
// вызывать её из любого места кода.
var add_table_records = function () {
//Когда по запросу в $.get будет возвращен ответ выполнить callback-функцию, которая выведет в консоль этот ответ и далее передаст
//его функции nadle_answer, которая уже отрисует его на страничке
$.when( $.get( "/server/list" )).done( function( data ) {
console.debug("Recevied data from /server/list api:");
console.debug(data);
handle_answer(data);
});
}
var handle_answer = function (data) {
// Разбиваем полученные данные по \n - переносу строки и превращаем в список
var lines = data.split("\n");
// Перебираем каждый элемент списка
lines.forEach(function(entry) {
if ( entry ) {
// Парсим в json текущий элемент
var entry_jsoned = JSON.parse(entry);
// Генерим html для tr-элемента
element_html = '<tr id="data'+entry_jsoned.user_id+'"><td id="userId">'+entry_jsoned.user_id+'</td><td id="name">'+entry_jsoned.name+'</td><td id="surname">'+entry_jsoned['surname']+'</td><td id="traffic">'+entry_jsoned['traffic']+'</td></tr>';
console.debug("Generated html is:");
console.log(element_html);
// Вставляем html после селектора table_head
table_head.after(element_html);
}
});
};
var handle_click = function(event) {
console.debug("Button pressed. Data recevied is:");
console.debug(event.data)
// Формируем Url для нашей ручки add в соответствии с данными, полученными по селекторам в форме. В event.data - находится та информация которую мы передавали по
// нажатию кнопки
var url = '/server/add?user_id='+event.data.id.val()+'&name='+event.data.name.val()+'&surname='+event.data.surname.val()+'&traffic='+event.data.traffic.val()
console.debug("Url for user add");
console.debug(url);
//Когда по запросу в $.get будет возвращен ответ выполнить callback – функцию которая обновит таблицу со свежеполученными данными
$.when( $.get( url )).done( function( data ) {
console.debug("Get all elements except head and remove then:");
console.debug(table_head.nextAll('tr'));
table_head.nextAll('tr').remove();
add_table_records();
});
};
// Если нажата кнопка, вызываем функцию handle_click и передаем ей набор селекторов в json-объекте откуда можно взять данные
traffic_info_button.on('click', { id : traffic_info_id, name : traffic_info_name, surname: traffic_info_surname, traffic: traffic_info_traffic }, handle_click);
//Здесь мы и
add_table_records();
</script>
</body>
</html>
Что мы сделали:
- Перед таблицей мы добавили форму с 4-мя полями и кнопку button_submit.
- Каждое поле у нас обладает своим собственным Id – и мы написали на каждое селектор чтобы уметь к нему обращаться по всему нашему коду. Например:
Означает – сохранить в переменной traffic_info_surname селектор который указывает на элемент c id surname находящийся где-то в селекторе traffic_info. А traffic_info мы определили еще раньше.var traffic_info_surname = traffic_info.find("#surname")
- Мы вынесли код которым отрисовывали записи в таблице в отдельную функцию. Это нужно для того, чтобы уметь вызывать этот код из любого места, в частности когда мы будем добавлять запись в базу – хочется в реалтайме обновлять эти записи на страничке. Общий алгоритм такой: удаляем все записи в таблице, перечитываем их и добавляем заново полученные из файла.
- По нажатию кнопки с селектором traffic_info_button – мы вызываем функцию которая, на основе данных из наших полей ввода, генерит url который мы потом передаем в jquery $.get метод.
- Мы дополнительно вывели в консоль массу полезной и не очень информации. По этому debug'у можно примерно понять, как работает код.
В браузере теперь все это будет выглядеть так:
Теперь мы можем добавлять пользователей и данные по ним просто нажимая кнопку.
5. Заключение
Мы написали frontend + backend. Поговорили об общих принципах работы схемы, научились отлаживать js код и коснулись вопросов использования современных инструментов (flask, bootstrap и jquery) в нашей реализации сервиса. Backend помогает нам получить данные из любого источника, отдать их frontend'у в виде ручек, а стили мы подгружаем из bootstrap на сайте которого есть масса примеров.
В следующих частях мы настроим выгрузку данных из netflow в этот сервис для сбора и отображения трафика. А потом, прикрутим наш сервис к базе данных mongo и обеспечим ему отказоустойчивость.
Попробуем настроить Nginx и обоснуем, зачем нужно это делать. Также поговорим про модальные окна и попробуем кастомизировать css bootstrap и используем шаблонизаторы.
Комментарии (15)
ahmpro
21.10.2015 08:26Я у себя вместо очереднего велосипеда просто поставил Whooey github.com/wooey/wooey и написал скриптов :)
kyzia
21.10.2015 11:08Но, кажется, написать свой микросервис на Питоне занимает ровно столько времени, сколько нужно чтобы скачать чужой и запустить его :)
oxpa
21.10.2015 11:00Каждый админ пишет свой веб сервис.
Я свой писал с flask, redirs, celery и angular.
Идея, кажется, витает в воздухе ;)kyzia
21.10.2015 11:06-1Ну парадигма называется — микросервисы (вот, например, пруф: www.infoq.com/articles/boot-microservices ).
Кажется сейчас очередной виток её развития.oxpa
21.10.2015 11:19-1gist.github.com/oxpa/1afb40770cafc0af0c38 просто оставлю это здесь, раз уж мы начали про flask.
Ещё есть пара интересных штук, но они написаны под воркеров и angular
homecreate
21.10.2015 22:29+3В итоге получился бэкдор, самый натуральный (ну или шелл, кому как больше нравится). Как-то вопросы безопасности решены?
kyzia
21.10.2015 22:56Ну первая часть — она про знакомство с технологиями, вообще и в принципе.
Но спасибо за идею.
Может быть в следующих частях статьи я изображу какой-нибудь авторизующий декоратор на методы.
В принципе — ничто не мешает проставить пару кук в сервисе, поднять oauth, разместить сервис за nginx с basic http authorization/auth_request через какой-нибудь скрипт в xinetd.
wiygn
И сразу же по установке пакетов для Python. Почему apt-get, а не православный pip? Не самый очевидный выбор, на мой взгляд.
kyzia
Потому что на production сервере не должно стоять dev-пакетов.
Иными словами, нужно стремиться к тому чтобы все файлы на сервере обладали принадлежностью к каким-либо пакетам.
Неорганизованный pip/pear/gem приводит к бардаку в системе и поломкам в самых неожиданных местах.
Bkmz
virtualenv? Вполне нормальный способ, и там нифига не «dev-пакеты»
kyzia
Ну да, почему бы и нет.
Можно использовать docker, virtualenv, катить бинари каким-нибудь chef'ом.
Но, позволю себе заметить, что правильная настройка всего этого — тема для отдельной статьи.
Deb пакеты — это инфраструктура с минимальными трудозатратами из коробки.
Именно то, что нужно для маленького, наколеночного сервиса.
usbmonkey
А теперь представьте, что весь многокомпонентный сервис вам как-то нужно раскатывать на сервера. Как вы себе представляете деплой при помощи pip'a?
А debain-пакетах все сильно проще. Делайте свой пакет, указываете в Depends все необходимое — получаете сервис по кнопке.
ahmpro
не спорю, что deb-пакеты самый простой и быстрый способ, но в последнее время весь деплой на ansible, времени на написание плейбуков и ролей не сильно больше тратится