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

    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 и используем шаблонизаторы.

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 24

      –3
      И сразу же по установке пакетов для Python. Почему apt-get, а не православный pip? Не самый очевидный выбор, на мой взгляд.
        +4
        Потому что на production сервере не должно стоять dev-пакетов.
        Иными словами, нужно стремиться к тому чтобы все файлы на сервере обладали принадлежностью к каким-либо пакетам.

        Неорганизованный pip/pear/gem приводит к бардаку в системе и поломкам в самых неожиданных местах.
          –1
          virtualenv? Вполне нормальный способ, и там нифига не «dev-пакеты»
            +4
            Ну да, почему бы и нет.
            Можно использовать docker, virtualenv, катить бинари каким-нибудь chef'ом.

            Но, позволю себе заметить, что правильная настройка всего этого — тема для отдельной статьи.
            Deb пакеты — это инфраструктура с минимальными трудозатратами из коробки.
            Именно то, что нужно для маленького, наколеночного сервиса.
              0
              Да господи. Поставьте Вы в virtualenv всё, что нужно, и до кучи pyinstaller. Соберите им хоть в один файл, его в deb пакет и разносите по серверам. И уже будет не столь важно, debian 7 или 8 на сервере: всё будет работать, и ничего лишнего на сервере.
                0
                И тогда мы вычеркиваем половину статьи (она и так получилась слишком объемной), потому что начинаем рассуждать про сборку пакетов и virtualenv.

                  0
                  virtualenv на самом деле лучшая практика.
                    0
                    Это вопрос еще. Если вы собираете venv на серверах с одной ОС а используете на других, с другим окружением — вы огребаете)

                    В пакете то вы, хотя бы, фиксируете наличие определенного окружения, через зависимости.

                    Другой разговор — если говорить про venv в контексте докероподобного образа (ну или, может, вы чрут захотите использовать). Но тогда и окружение вам при деплое все нужно будет собирать, а не только venv.

                    Иными словами, venv должен как то быть привязан к версии ОС.
                      0
                      если вы используете на dev, build и live разные ОС, разные версии, вообще разные окружения то огребаете в любом случае :)

                      никто не раскатывает venv без фиксированных зависимостей, а кто так делает, либо понимает, что он делает, либо сам себе злобный буратино.

                      и не надо мешать в одну кучу системные зависимости(те же deb, rpm, ...) и программные (pip, npm, ...), которые опираются на системные, у них разный жизненный цикл, цели и задачи.

                      ну а вообще не очень понятен контекст: мы говорим про небольшие наколеночные сервисы, в которых можно делать любые допущения, главное чтобы хоть как-то работало или же про более-менее нормальный продакшен, где множество заинтересованных сторон с своими хотелками, требованиями и ограничениями.

                      PS: за себя могу сказать, что я согласен с тем, что для python «virtualenv на самом деле лучшая практика», однако сейчас использую его в редких случаях, из-за его избыточности в текущих задачах.
          +1
          А теперь представьте, что весь многокомпонентный сервис вам как-то нужно раскатывать на сервера. Как вы себе представляете деплой при помощи pip'a?
          А debain-пакетах все сильно проще. Делайте свой пакет, указываете в Depends все необходимое — получаете сервис по кнопке.
            0
            не спорю, что deb-пакеты самый простой и быстрый способ, но в последнее время весь деплой на ansible, времени на написание плейбуков и ролей не сильно больше тратится
          0
          Я у себя вместо очереднего велосипеда просто поставил Whooey github.com/wooey/wooey и написал скриптов :)
            0
            Но, кажется, написать свой микросервис на Питоне занимает ровно столько времени, сколько нужно чтобы скачать чужой и запустить его :)
            0
            Каждый админ пишет свой веб сервис.
            Я свой писал с flask, redirs, celery и angular.

            Идея, кажется, витает в воздухе ;)
              –1
              Ну парадигма называется — микросервисы (вот, например, пруф: www.infoq.com/articles/boot-microservices ).
              Кажется сейчас очередной виток её развития.
                –1
                gist.github.com/oxpa/1afb40770cafc0af0c38 просто оставлю это здесь, раз уж мы начали про flask.
                Ещё есть пара интересных штук, но они написаны под воркеров и angular
                –1
                Году в 2002 писал такое на perl без этих ваших jquery.
                  0
                  А это 2015 год и в наше время динозавры уже вымерли, а для того, чтобы не умереть с голоду, не обязательно охотится на мамонтов.
                +3
                В итоге получился бэкдор, самый натуральный (ну или шелл, кому как больше нравится). Как-то вопросы безопасности решены?
                  0
                  Ну первая часть — она про знакомство с технологиями, вообще и в принципе.
                  Но спасибо за идею.
                  Может быть в следующих частях статьи я изображу какой-нибудь авторизующий декоратор на методы.

                  В принципе — ничто не мешает проставить пару кук в сервисе, поднять oauth, разместить сервис за nginx с basic http authorization/auth_request через какой-нибудь скрипт в xinetd.

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


                  Не нужно так делать. Используйте logging.

                  import logging
                  log = logging.getLogger('modulename')
                  
                  def main():
                      log.debug('Hey! Main function started.')
                      try:
                          # do some stuff
                          raise RuntimeError('Test')
                      except (Exception) as e:
                          log.error('Uncaught error occupied', exc_info=e)
                          raise
                  
                  if __name__ == '__main__':
                      logging.basicConfig(level=logging.DEBUG)
                      main()
                  


                  Учитывая, что тесты Вы не пишите, в будущем, если вдруг внезапно что-то перестанет работать, будет легче найти ошибку.
                    0
                    Вы исходите из ложной в данном случае предпослыки что код написанный выше надо улучшить усложняя.
                    В данном случае — вы не правы.
                    Я, правда, знаю про logging. Я также знаю что код не идеален. Но, предположим, что код рассчитан на школьников и студентов, вообще не имеющих представления об веб разработке.

                    Предложите, пожалуйста, вариант без включения дополнительных модулей. Предложите упрощение кода без усложнения?
                      +1
                      Не знаю, что может быть легче трех строк кода:

                      import logging
                      log = logging.getLogger('modulename')
                      logging.basicConfig(level=logging.DEBUG)
                      


                      Дальше вместо print, использовать log:

                      log.debug("Message")
                      


                      Пример выше всего лишь более подробный, и logging — находится в стандартной библиотеке python. Дополнительно устанавливать ничего не нужно.
                      Я считаю, что написать «log.debug('message')» не сложнее, чем написать print('message'), и этому нужно учить сразу в том числе школьников, и новичков. Вредные привычки, приобретенные во время обучения искореняются с большим трудом.

                      import flask
                      import logging
                      app_test = flask.Flask(__name__)
                      log = logging.getLogger(__name__)
                      
                      @app_test.route("/ping")
                      def ping():
                         return "pong"
                      
                      @app_test.route("/add")
                      def add():
                         log.debug(flask.request.args.to_dict())
                         return str(flask.request.args)
                      
                      @app_test.route("/remove")
                      def remove():
                         log.debug(flask.request.args.to_dict())
                         return str(flask.request.args)
                      
                      if __name__ == "__main__":
                         logging.basicConfig(level=logging.DEBUG)
                         app_test.run(host='0.0.0.0')
                      


                      Это существенное усложнение?
                        0
                        Теперь давайте еще поговорим что все в питоне это объект, а вот тут мы инициализируем класс :)
                        Это существенное усложнение?

                        Я думал поговорить про это во второй части.

                        Вы все говорите правильно, и предлагаете правильный код, спасибо.
                        Надеюсь что тот, кто доберётся до вашего комментария возмёт его на вооружение.

                  Only users with full accounts can post comments. Log in, please.