Pull to refresh

Сервис для системного администратора. Часть 1

Python *JavaScript *
image

1. Предисловие


Задачи системного администратора — разнообразны. Работа в консоли — создание пользователей, тестирование, установка и конфигурация пакетов на серверах, просмотр логов и трафика, настройка сети и туннелей. Работа с железом — установка оборудования и документацией, написание планов работ, описание работы сервисов.

Системные администраторы имеют большое количество скриптов для автоматизации. Хранятся они, обычно, в их домашних папках (и хорошо если не только там, но и в CVS), периодически апдейтятся на новую функциональность. Как правило, использовать такие скрипты могут лишь те же люди которые их пишут (а, иначе, последствия могут быть самыми разрушительными).

Таким образом — задачи по автоматизации часто нуждаются в простом GUI для удобного управления, упрощения. Например — сбор трафика. Или возможность откатывать бекапы/релизы по кнопке (даже если кто-то катится пакетами через SCM систему). Или менять Mysql master без подглядывания консоль ( какое-то количество ошибок возникает из-за неверно забитой в команды, не на том сервере).

Предание 1: Bash скрипты и dialog в production
На одной из моих прошлых работ после установки сервера и системы на них, мы запускали на сервере bash-скрипт который, используя dialog, позволял настроить экосистему для разработчиков. В нем была возможность отметить галочками — что конкретно хотелось бы сейчас настроить на этом сервере. Впрочем, позже на сервера начали накатывать конфигурацию используя puppet.

Предание 2: Решение для сбора трафика на базе Netflow
На другой работе вся информация по трафику собиралась с серверов с помощью Netflow и сохранялась в базу данных с помощью bash скрипта. Другим скриптом содержимое базы данных каждый месяц выгружалось в xls-файл.


Современные технологии предлагают большое количество вариантов для быстрого написания своего сервиса с приличным GUI. Мы разберем пример клиент-серверного взаимодействия и напишем наш, собственный REST api сервис используя технологии Jquery, Bootstrap, язык программирования Python и библиотеку python-flask. Хранить данные мы будем в текстовом файле.

В качестве клиента к нашему REST api будет выступать обычная html страничка с некоторым javascript кодом.

Статья рассчитана на системных администраторов которым изредка приходится делать небольшие наколеночные решения. Работать будем в операционной системе Linux Ubuntu 12.04. Тот же набор технологий можно использовать в любой другой ОС (Windows, Mac, Freebsd ).

2. Про технологии




REST — набор общепринятых рекомендаций, следуя которым можно построить backend под который стороннему разработчику будет удобно писать клиентское приложение и/или frontend. Забегая вперед — мы немного отклонимся от этих рекомендаций, и будем использовать изначально идемпотентный метод GET для добавления новой информации на сервер.

Примечание 4: REST
Рекомендации — рекомендуют, и их можно игнорировать ровно в том объеме в котором хочется. Но если вы задумаете большой сервис с разлапистым API — следуя REST можно значительно уменьшить количество бардака вокруг него. Рекомендую прочитать (про идемпотентность там тоже есть): habrahabr.ru/company/yandex/blog/265569 (15 тривиальных фактов о правильной работе с протоколом HTTP)

Примечание 5: Frontend
Frontend — это то что выполняется на стороне пользователя, в его браузере. Как правило — это какой-либо javascript код. Backend — то что выполняется на сервере (грубо говоря, та программа, что отвечает на 80-ом порту).

Bootstrap — набор стилей и шаблонов html который позволит нам не думать над оформлением нашей страницы, использовать готовые элементы.

Jquery — javascript библиотека которая расширяет возможности языка и позволяет использовать готовые удобные функции для, например, формирования GET запросов.

Python-flask — библиотека для языка Python которая позволит в несколько строчек кода написать web-сервер.

3. Делаем backend


image

Ставим 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. Теперь мы можем зайти на него в браузере:

image

В консоли видим:

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

Добавляем методы в server.py:
# -*- 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:

image

Удалим Midnight Commander:

image

А теперь поставим:

image

Примечание 7. Что такое 'ручка'
Ручка — в данном случае это то, за что можно “дернуть”, или иначе — зайти с браузера по определенному урлу.

В этом примере я перевесил сервис на 80 ый порт, дописав port=80 параметром для app.test. Этот порт используется по умолчанию браузером, поэтому нет необходимости дописывать :80 к урлу.

3.5 Выводим полученные аргументы в ответе

Вернемся к нашей первой заготовке.
В backdoor — ручке мы передаем серверу определенные аргументы — cmd, которые должны выполниться на сервере.

Давайте выведем аргументы которые мы посылаем серверу — в ответе (кстати, с помощью функции print удобно выводить их прямо в консоль) этого самого сервера. Приведем server.py к следующему виду (не забываем его перезапустить после изменения кода):

Содержимое файла 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 вернет нам то, что мы послали:

image

А в консоли:

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 который умеет отсылать нам обратно то, что ему было отправлено. Но мы хотим красивые кнопочки работать с ручками из frontend'а. И данные нам нужно не только возвращать, но и где-то сохранять.

Сделаем небольшой аналог базы данных через текстовый файл, доступ в которую обеспечивается некоторым количеством http — ручек.

И давайте же запишем в него что-нибудь!

Немного улучшаем наш код.
Содержимое файла /var/www/server.py
#!/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 так же как раньше:

image

Что мы получаем в консоли ( обратите внимание на доп. информацию которую мы выводим с помощью функции 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:

image

Есть так же ручка /list которая отдаст нам все что есть в файле data.txt:

image

А теперь дернем ручку /remove и увидим что в data.txt у нас эта строчка пропадет:

image

root@golem:/var/www# cat data.txt
{"surname": "Batareikin", "traffic": "12248", "name": "Dmitry", "user_id": "2"}

И действительно, попросив информацию по пользователю с user_id = 1 мы получим false:

image

Чем плохо хранить данные в файле — следует из метода /remove который сначала все читает в dict_list, потом удаляет все что есть в файле, и пишет все что он прочитал за исключением той информации которую писать не нужно ( element['user_id'] != user_id ).

По факту получается, что, каждый раз, мы удаляем и пересохраняем целый файл, и, пока он маленький — так делать можно. Но количество записей в файле будет расти и операция удаления записи будет занимать все более продолжительное время.

Поэтому, в production среде лучше использовать полноценную базу данных в которой решены подобные вопросы. Если говорить про простые, файловые базы то можно, например, взять sqlite (модуль sqlite3 в python). Её можно использовать если нагрузка на сервис будет относительно невысока.

Для более серьезных проектов можно использовать mysql или, что мне нравится больше — mongodb (как и когда какую базу лучше использовать — возможно, поговорим в следующих частях статьи).

4. Делаем frontend


image

Итак, мы сделали backend который пишет в файл то, что мы ему послали. И умеет отдавать то, что мы записали и может удалять элементы по user_id.

Теперь хочется красиво отобразить это на страничке таким образом, чтобы добавлять и удалять записи можно было без обновления этой самой странички. Будем использовать ajax.

Примечание 8: Что такое ajax.
Когда-то давно, когда интернет был маленьким, а web страницы грузились с модема по полминуты — любое действие со страницей подразумевало её полную перерисовку. Однако шло время, скорость росла и менялся паттерн взаимодействия пользователя со страницей. Технология ajax подразумевает что страница является, своего рода, «тонким клиентом» по отношению к backend'у. Теперь, если пользователь нажимает кнопку — в режиме реального времени отрабатывает некоторый js-код, который меняет DOM дерево страницы прямо в браузере, добавляя или удаляя элементы. Примером здесь может служить, например, поисковая строка в Яндексе — после нажатия кнопки «Найти» — перерисовки всей страницы не происходит, а прямо в текущий html дорисовывается информация полученная с сервера — Backend'а.

4.1 Настраиваем отдачу html и статики

Если в backend у нас работает некоторый код на Python, то в браузере, для отрисовки страницы, используется другой язык — html. И прежде чем он начнет работать, его как-то надо отдать. Поэтому, добавим(изменим) в нашем server.py следующие строчки:
/var/www/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>

И посмотрим как она отображается в браузере:

image

А в консоли видим что кто-то действительно пришел и попросил нашу страничку:


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



Примечание 9: bootsrap
Про то, как подключать bootstrap на сайте bootstrap:
getbootstrap.com/getting-started/#template

Примечание 10: bootstrap примеры
Больше примеров (чтобы посмотреть код — открываем пример, нажимаем в нем правой клавишей и в ниспадающем меню «посмотреть исходный код»):
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.

Примечание 11: Тег style
В теге style в head — я переопределил отступ для всех элементов тега body – иначе наша таблица вплотную примыкала бы к границам экрана. Рекомендую заглянуть в (отрисовка всяких всплывающих окошек и прочего динамического контента):
getbootstrap.com/javascript

Итого, получаем в браузере вот такую таблицу:

image

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.

Html, содержимое файла /var/www/index.htm:
<!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>


В браузере:

image

С помощью расширения firebug (его можно поставить как дополнение в Firefox) мы можем посмотреть информацию в консоли js (можно так же воспользоваться стандартными средствами браузера).

В данном случае мы в консоль вывели два объекта – текущий документ и найденный с помощью метода .find объект с id=userId. Кроме того я вывел в консоль полученный с помощью Jquery метода $.get текст полученный из ручки /list.
С каждым из этих объектов из консоли можно взаимодействовать (нужно кликнуть правой клавишей мыши на слове Object рядом с td#userId, выбрать – использовать в командной строке ), например, вызвав метод .empty() мы увидим что содержимое элемента пропадет из нашего DOM – дерева:

image

В данном случае – мы удаляем “1” под полем UserId:

image

По методам для объекта так же работает автодополнение – можно посмотреть что каждый из них делает. Кроме того, доступные методы для Object и для td#userId (это разные объекты) – будут разными. У первого, например, нет метода .innerHTML.

4.5 Отобразим загруженную информацию в таблице

Теперь, зная как взаимодействовать с объектами – отобразим все полученное по ручке /list в нашей таблице. Здесь, кроме всего прочего, я использую jquery метод .after который позволяет вставить сгенеренный html код прямо после элемента – заголовка нашей таблицы, которому я проставил id=«head_tr».

Html, содержимое файла /var/www/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 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 объекта.

Что же получилось у нас в браузере:

image

Мы видим, что с помощью javascript мы “дорисовали” таблицу вставив в нее еще два tr-элемента с информацией которую мы получили из ручки /server/list. Отлично.

4.6 Отправим данные на сервер не перегружая страницу

Теперь, давайте добавим кнопку которая будет, например, добавлять, пользователей в нашу мини-базу данных. Для этого воспользуемся элементом horizontal form все того же bootsrap'a ( getbootstrap.com/css/#forms-horizontal ) только немного его поправим (например, возьмем другую кнопку и сделаем отступ между элементами).
А для добавления пользователей мы будем использовать ручку /add.

Примечание 12: Конфликт user id
Добавляя пользователя с id который уже существует — будем получать в файле записи с одинаковыми id.

Html и js будет выглядеть уже следующим образом:
Содержимое файла /var/www/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; }
    .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 – и мы написали на каждое селектор чтобы уметь к нему обращаться по всему нашему коду. Например:
    var traffic_info_surname = traffic_info.find("#surname")
    Означает – сохранить в переменной traffic_info_surname селектор который указывает на элемент c id surname находящийся где-то в селекторе traffic_info. А traffic_info мы определили еще раньше.
  • Мы вынесли код которым отрисовывали записи в таблице в отдельную функцию. Это нужно для того, чтобы уметь вызывать этот код из любого места, в частности когда мы будем добавлять запись в базу – хочется в реалтайме обновлять эти записи на страничке. Общий алгоритм такой: удаляем все записи в таблице, перечитываем их и добавляем заново полученные из файла.
  • По нажатию кнопки с селектором traffic_info_button – мы вызываем функцию которая, на основе данных из наших полей ввода, генерит url который мы потом передаем в jquery $.get метод.
  • Мы дополнительно вывели в консоль массу полезной и не очень информации. По этому debug'у можно примерно понять, как работает код.

В браузере теперь все это будет выглядеть так:

image

Теперь мы можем добавлять пользователей и данные по ним просто нажимая кнопку.

5. Заключение


Мы написали frontend + backend. Поговорили об общих принципах работы схемы, научились отлаживать js код и коснулись вопросов использования современных инструментов (flask, bootstrap и jquery) в нашей реализации сервиса. Backend помогает нам получить данные из любого источника, отдать их frontend'у в виде ручек, а стили мы подгружаем из bootstrap на сайте которого есть масса примеров.

В следующих частях мы настроим выгрузку данных из netflow в этот сервис для сбора и отображения трафика. А потом, прикрутим наш сервис к базе данных mongo и обеспечим ему отказоустойчивость.

Попробуем настроить Nginx и обоснуем, зачем нужно это делать. Также поговорим про модальные окна и попробуем кастомизировать css bootstrap и используем шаблонизаторы.
Tags:
Hubs:
Total votes 19: ↑13 and ↓6 +7
Views 38K
Comments Comments 24