Пишем простой buildserver на Python (Часть 1)

от
Прочее   python

В этой статье я расскажу о том, как написать простой build server с использованием языка программирования Python.

Для начала хорошо было бы знать, что такое build server.
Это специальное ПО, предназначенное для обеспечения непрерывной интеграции.

Непрерывная интеграция (Continuous Integration, CI) - практика разработки ПО, при которой выполняются частые сборки проектов, что позволяет быстро выявить и решить различные проблемы.

Итак, билд сервер должен уметь следующее:

- Получить исходный код из репозитория.
- Собрать проект (установить сторонние библиотеки, используемые в нём, скомпилировать и т.п.).
- Выполнить тесты.
- Задеплоить готовый проект.

Так как я не собираюсь делать убийцу TeamCity или Travis CI, то остановимся на этом:

- Клонирование из git репозитория.
- Выполнение шагов, описанных в конфигурации проекта.
- Просмотр лога сборки.

Что нам понадобится:

- Python
- Buildout
- Flask
- Tornado
- psycopg2
- PyZMQ

Python, я думаю, в представлении не нуждается.

Buildout. Позволяет устанавливать сторонние библиотеки, не прибегая к virtualenv и не засоряя систему.
А ещё у него есть куча рецептов, которые позволяют вытворять всякие прикольные штуки
вроде генерации скриптов, конфигов из шаблонов, интеграции с различными библиотеками и т.п.
Можно написать какой-нибудь свой рецепт. Всё ограничено только полётом фантазии.

Flask -- это микро-фреймворк, предназначенный для написания веб-приложений.
Tornado -- неблокирующий веб-сервер и веб-фреймворк.
psycopg2 -- питоновские биндинги для PostgreSQL.
PyZMQ -- обеспечивает поддержку ZMQ.

ZMQ -- библиотека, позволяющая создать систему очереди сообщений.
Подробнее о ZMQ можно узнать например здесь http://habrahabr.ru/post/198578/

Подготовка проекта и установка зависимостей

Первым делом создаём директорий для проекта:

$ mkdir buildserver
$ cd buildserver

Создаём файл setup.py со следующим содержимым:

$ editor setup.py

  1. from setuptools import setup, find_packages
  2.  
  3. setup(
  4.     name='buildserver',
  5.     version='0.1',
  6.     description='Simple buildserver',
  7.     packages=find_packages('src'),
  8.     package_dir={'': 'src'},
  9.     install_requires=[
  10.         'flask',
  11.         'psycopg2',
  12.         'pyzmq',
  13.         'tornado'
  14.     ]
  15. )

Создаём файл buildout.cfg:

$ editor buildout.cfg

  1. [buildout]
  2. develop = .
  3. parts = buildserver
  4.  
  5. [buildserver]
  6. recipe = zc.recipe.egg
  7. eggs = buildserver
  8. interpreter = py

Создаём пакет buildserver:

$ mkdir src/buildserver -p
$ touch src/buildserver/__init__.py

Перед установкой зависимостей проекта, в систему придётся поставить некоторые пакеты,
которые требуются сборки этих зависимостей.
А именно:

python-dev
libzmq3-dev
postgresql-server-dev-9.3

Ну и сам постгрес надо будет поставить, если не стоит.

В убунте и прочих debian-based дистрибутивах достаточно выполнить:

$ sudo apt-get install python-dev libzmq3-dev postgresql-9.3 postgresql-server-dev-9.3

Ставим buildout и зависимости для проекта:

$ wget http://downloads.buildout.org/2/bootstrap.py
$ python bootstrap.py
$ bin/buildout

Настройка PostgreSQL

Устанавливаем пароль для пользователя postgres:

$ sudo passwd postgres

Эта команда запросит ваш пароль, новый пароль для postgres, и повтор нового пароля.

Логинимся:

$ su postgres

Создаём юзера:

$ createuser -sdrP kilte

Конечно имя пользователя может быть каким угодно, но для большего удобства лучше создать пользователя
с таким же именем, под каким вы залогинены. Это позволит не указывать имя пользователя каждый раз,
когда вы запускаете psql.

Выходим:
$ exit

Заходим в psql:

$ psql postgres

Создаём базу данных для нашего приложения:

create database buildserver owner kilte;

На этом настройку postgres можно считать оконченой.


Bower

Bower - это менеджер зависимостей для фронтенда.

http://bower.io/ - Оф. сайт
http://nano.sapegin.ru/all/bower - Инфа на русском.

Для того, чтобы установить bower, нужно поставить nodejs и npm.
Я недолюбливаю npm и всё, что с ним связано, потому что оно выкачивает десятки мегабайт непонятно чего.
Мы пойдём более простым путём.
Bower портирован на PHP и мы спокойно можем скачать один файл, и начать пользоваться.

$ wget http://bowerphp.org/bowerphp.phar

Можете теперь сделать с ним всё, что угодно.
У меня же для подобных штук в домашнем директории есть директорий bin, куда я складываю все бинарники.
~/bin прописан в $PATH, что позволяет мне запускать все файлы из ~/bin, не набирая абсолютный путь до исполняемого файла.

Кому-то может такое и не понравится, потому можно сделать вот что:

$ sudo mv bowerphp.phar /usr/local/bin/bowerphp

Конечно нужно не забыть сделать этот файл исполняемым:

$ sudo chmod +x /usr/local/bin/bowerphp

Теперь пробуем выполнить:

$ bowerphp

Создаём в корне проекта два файла .bowerrc и bower.json

В .bowerrc пишем:
  1. {
  2.     "directory": "web/vendor"
  3. }

Здесь мы определили путь к директорию, куда будут установлены зависимости, описанные в bower.json

В bower.json:
  1. {
  2.     "name": "buildserver",
  3.     "private": true,
  4.     "dependencies": {
  5.         "angular": "1.3.*",
  6.         "angular-route": "1.3.*",
  7.         "angular-websocket": "*"
  8.     }
  9. }

Ну здесь всё понятно, а что не понятно, смотрите доку на офф сайте.

Выполняем:

$ bowerphp install

После чего получим в корне проекта директорий web/vendor, в котором лежат описанные в конфиге зависимости.


Настройка Nginx

Ещё не стоит? Почему? А ну быстро ставим. Хе-хе.

$ sudo apt-get install nginx

Пишем конфиг для него:

$ sudo editor /etc/nginx/sites-available/buildserver.conf

Содержимое конфига

Путь к проекту не забудьте заменить.

Врубаем хост:

$ sudo ln -s /etc/nginx/sites-available/buildserver.conf /etc/nginx/sites-enabled/
$ sudo service nginx restart

В /etc/hosts: 127.0.0.1 buildserver

Backend

Бэкэнд будет состоять из нескольких частей:

Worker -- отвечает за сборку проектов.
Web -- REST API, написанное на фреймворке Flask
Broadcast -- приложение, написанное на Tornado, которое позволит отображать ход сборки в режиме реального времени.

Все они будут связаны между собой с помощью ZeroMQ.
Из веб приложения отсылается команда воркеру на сборку проекта.
При просмотре билда tornado будет считывать лог и отправлять его клиенту через WebSockets.
По окончанию сборки воркер сообщает об этом tornado, а то в свою очередь отправляет сообщение клиенту.

Для начала давайте создадим таблицы в БД.

$ psql buildserver

Выполняем:
  1. CREATE TABLE "projects" (
  2.     "id" SERIAL PRIMARY KEY,
  3.     "name" VARCHAR(70) NOT NULL,
  4.     "description" VARCHAR(200) NOT NULL DEFAULT '',
  5.     "url" VARCHAR(200) NOT NULL
  6. );
  7.  
  8. CREATE TABLE "builds" (
  9.     "id" SERIAL PRIMARY KEY,
  10.     "project_id" INTEGER NOT NULL REFERENCES "projects" ("id") ON DELETE RESTRICT,
  11.     "start_date" INTEGER NOT NULL,
  12.     "finish_date" INTEGER NOT NULL,
  13.     "state" VARCHAR(10) NOT NULL
  14. );

REFERENCES "projects" ("id") ON DELETE RESTRICT означает, что поле ссылается на поле id в таблице projects.

Если мы попытаемся создать билд, указав project_id, который отсутствует в таблице projects, то у нас ничего не получится.
При удалении проекта, нужно будет удалить сначала все сборки.
В ином случае postgres просто пошлёт нас куда подальше.
Подробности здесь: http://postgresql.ru.net/manua...l#DDL-CONSTRAINTS-FK

Большинство пакетов для работы с базой данных реализуют Database API Specification 2.0
https://www.python.org/dev/peps/pep-0249/

Вся работа сводится к созданию подключения, получению курсора, выполнению запроса и коммита транзакции.

  1. import psycopg2
  2.  
  3. conn = psycopg2.connect('postgresql://username:password@localhost/database')
  4. cur = conn.cursor()
  5. cur.execute('INSERT INTO "tablename" ("name", "desc") VALUES (%s, %s)', ('name-val', 'desc-val'))
  6. try:
  7.     conn.commit()
  8. except:
  9.     # При возникновении исключения откатываем транзакцию.
  10.     # В ином случае следующий запрос нельзя будет совершить.
  11.     conn.rollback()
  12. conn.close()


Создаём файл src/buildserver/app/repositories.py
В нём будут располагаться классы, необходимые для работы с БД.
repositories.py

Чтобы выполнять команды в шелле из питона, воспользуемся модулем subprocess.
Почитать о нём на русском языке можно здесь.

src/buildserver/app/cmd.py

Если вы читали материал, приведённый по ссылке выше, то никаких вопросов возникнуть не должно.

Настройки приложения будут располагаться в src/buildserver/app/settings.py:

  1. import os
  2.  
  3. # Режим отладки
  4. DEBUG = True
  5. # DSN для подключения к БД
  6. PG_DSN = 'postgresql://kilte:1234@localhost/buildserver'
  7.  
  8. # Адрес, на который завязывается ZMQ сокет для отправки сообщения о том, что нужно начать сборку проекта
  9. TASK_NEW_PUBLISHER = 'tcp://127.0.0.1:8000'
  10. # Адрес, на который завязывается ZMQ сокет для отправки сообщения о том, что сборка завершена
  11. TASK_COMPLETE_PUBLISHER = 'tcp://127.0.0.1:8001'
  12.  
  13. # Адрес и порт для tornado приложения
  14. BROADCAST = {
  15.     'address': '127.0.0.1',
  16.     'port': 8888
  17. }
  18.  
  19. # Путь к корневому директорию проекта
  20. ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../'))
  21. # Путь к логам сборок
  22. LOGS_PATH = os.path.join(ROOT_PATH, 'logs')
  23. # Путь к директорию, в который будут клонироваться проекты
  24. BUILDS_PATH = os.path.join(ROOT_PATH, 'builds')

Создаём директории logs и builds в корне проекта.

Было бы неплохо иметь централизованный доступ к логам сборок.
src/buildserver/app/log.py

Теперь, чтобы можно было импортировать только что созданные модули, необходимо сказать питону, что src/buildserver/app является пакетом.
Для этого просто создаём пустой файл с именем __init__.py в этом директории.

REST API

Читаем про REST https://ru.wikipedia.org/wiki/REST
Еще можно здесь почитать: http://eax.me/rest/
Ну и это: http://habrahabr.ru/post/181988/

API приложения будет выглядеть примерно следующим образом:

HTTP Метод | URL | Описание

GET /api/v1/projects - Получить список проектов
POST /api/v1/projects - Создать проект
GET /api/v1/projects/<pid> - Получить данные конктретного проекта
PUT /api/v1/projects/<pid> - Обновить проект
DELETE /api/v1/projects/<pid> - Удалить проект

POST /api/v1/projects/<pid>/build - Начать сборку проекта
GET /api/v1/projects/<pid>/builds/<bid> - Получить информацию о сборке

Переходим к реализации.

src/buildserver/web.py

REST API готово. Переходим к воркеру.
Но для начала давайте немного поиграемся с pyzmq.
Создадим в корне проекта два файла producer.py и publisher.py

producer:

  1. import zmq
  2.  
  3. context = zmq.Context()
  4. socket = context.socket(zmq.PULL)
  5. socket.connect('tcp://127.0.0.1:8000')
  6.  
  7. while True:
  8.     print socket.recv_json()

publisher:

  1. import zmq
  2.  
  3. context = zmq.Context()
  4. socket = context.socket(zmq.PUSH)
  5. socket.bind('tcp://127.0.0.1:8000')
  6.  
  7. for i in range(0, 10):
  8.     socket.send_json({'id': i})

Запускаем:

$ bin/py publisher.py
$ bin/py producer.py

На выходе получим:
  1. {u'id': 0}
  2. {u'id': 1}
  3. {u'id': 2}
  4. {u'id': 3}
  5. {u'id': 4}
  6. {u'id': 5}
  7. {u'id': 6}
  8. {u'id': 7}
  9. {u'id': 8}
  10. {u'id': 9}

Круто, не правда ли?
Удаляем publisher.py и producer.py, они нам больше не пригодятся.

Создаём воркер.

src/buildserver/worker.py


Теперь необходимо сделать отображение лога сборки в режиме реального времени.
src/buildserver/broadcast.py

Ну вот, как-то так.
Остаётся сделать фронтенд и научиться запускать всё это дело.
+5   6   1
4452