Пишем простой build server на python

1.2K
.
reaper
В этой статье я расскажу о том, как написать простой 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

from setuptools import setup, find_packages

setup(
    name='buildserver',
    version='0.1',
    description='Simple buildserver',
    packages=find_packages('src'),
    package_dir={'': 'src'},
    install_requires=[
        'flask',
        'psycopg2',
        'pyzmq',
        'tornado'
    ]
)


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

$ editor buildout.cfg

[buildout]
develop = .
parts = buildserver

[buildserver]
recipe = zc.recipe.egg
eggs = buildserver
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 пишем:
{
    "directory": "web/vendor"
}


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

В bower.json:
{
    "name": "buildserver",
    "private": true,
    "dependencies": {
        "angular": "1.3.*",
        "angular-route": "1.3.*",
        "angular-websocket": "*"
    }
}


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

Выполняем:

$ 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
.
reaper
Backend

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

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

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

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

$ psql buildserver

Выполняем:
CREATE TABLE "projects" (
    "id" SERIAL PRIMARY KEY,
    "name" VARCHAR(70) NOT NULL,
    "description" VARCHAR(200) NOT NULL DEFAULT '',
    "url" VARCHAR(200) NOT NULL
);

CREATE TABLE "builds" (
    "id" SERIAL PRIMARY KEY,
    "project_id" INTEGER NOT NULL REFERENCES "projects" ("id") ON DELETE RESTRICT,
    "start_date" INTEGER NOT NULL,
    "finish_date" INTEGER NOT NULL,
    "state" VARCHAR(10) NOT NULL
);


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

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

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

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

import psycopg2

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



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


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

src/buildserver/app/cmd.py (+/-)


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

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

import os

# Режим отладки
DEBUG = True
# DSN для подключения к БД
PG_DSN = 'postgresql://kilte:1234@localhost/buildserver'

# Адрес, на который завязывается ZMQ сокет для отправки сообщения о том, что нужно начать сборку проекта
TASK_NEW_PUBLISHER = 'tcp://127.0.0.1:8000'
# Адрес, на который завязывается ZMQ сокет для отправки сообщения о том, что сборка завершена
TASK_COMPLETE_PUBLISHER = 'tcp://127.0.0.1:8001'

# Адрес и порт для tornado приложения
BROADCAST = {
    'address': '127.0.0.1',
    'port': 8888
}

# Путь к корневому директорию проекта
ROOT_PATH = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../../'))
# Путь к логам сборок
LOGS_PATH = os.path.join(ROOT_PATH, 'logs')
# Путь к директорию, в который будут клонироваться проекты
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:

import zmq

context = zmq.Context()
socket = context.socket(zmq.PULL)
socket.connect('tcp://127.0.0.1:8000')

while True:
    print socket.recv_json()


publisher:

import zmq

context = zmq.Context()
socket = context.socket(zmq.PUSH)
socket.bind('tcp://127.0.0.1:8000')

for i in range(0, 10):
    socket.send_json({'id': i})


Запускаем:

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

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


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

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

src/buildserver/worker.py (+/-)



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


Ну вот, как-то так.
Остаётся сделать фронтенд и научиться запускать всё это дело.
.
reaper
Фронтенд будет написан с помощью AngularJS.

В директории web, что располагается в корне проекта создайте директорий templates и файлы application.js, index.html, style.css

index.html (+/-)


style.css (+/-)



Шаблоны (+/-)


application.js (+/-)


С фронтендом почти покончено.

Обновляем setup.py:

from setuptools import setup, find_packages

setup(
    name='buildserver',
    version='0.1',
    description='Simple buildserver',
    packages=find_packages('src'),
    package_dir={'': 'src'},
    install_requires=[
        'flask',
        'psycopg2',
        'pyzmq',
        'tornado'
    ],
    # Добавили точки входа
    entry_points={
        'console_scripts': [
            'broadcast=buildserver.broadcast:run',
            'web=buildserver.web:run',
            'worker=buildserver.worker:run'
        ]
    }
)


Выполняем bin/buildout, после чего будут сгенерированы bin/broadcast, bin/web и bin/worker
Запускаем их.

Создаём тестовый проект:

$ mkdir /tmp/testrepo && cd /tmp/testrepo && git init
$ editor .buildserver

Пишем туда следующее:
echo 'Step'
sleep 1
echo 'Step'
sleep 1
echo 'Step'
sleep 1
echo 'Step'
sleep 1
echo 'Step'
sleep 1
echo 'Step'
sleep 1
echo 'Step'
sleep 1
echo 'Step'
sleep 1


$ git add .buildserver && git commit -m "Init"

Открываем в браузере http://buildserver добавляем проект. В качестве урлы указываем /tmp/testrepo.
После сохранения проекта жмём Build, обновляем страницу и переходим к просмотру лога.

Если я нигде не ошибся и вы всё сделали правильно, то всё должно быть ok.

Ну а теперь домашнее задание. Гг.
Сделать отображение уведомления о том, что билд был добавлен в очередь на сборку.
Обновлять список сборок в режиме реального времени (добавление/изменение/удаление).
Причём не ддосить базу запросами, а задействовать zmq и tornado.

На этом всё. Исходный код проекта можно найти здесь: https://github.com/Kilte/buildserver
.
Ego vir viden
я так понял,без линукса вообще беда
.
selemet, Ну почему, можно и на венде попробовать, но я бы не стал. Слишком гемморно будет наверное. Тот же zmq я понятия не имею, как на венде ставить.

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

Вот тут описано, как это сделать http://istickz.ru/vagrant-and- ... dows/ Только рельсы не надо ставить Гг.
.
selemet
Ego vir viden
# reaper (29.11.2014 / 22:59)
selemet, Ну почему, можно и на венде попробовать, но я бы не стал. Слишком гемморно будет наверное. Тот же zmq я понятия не имею, как на венде ставить.

Можешь поставить в виртуалку какой-нибудь б
а может сразу на виртуалке ставить линукс и там работать? гг
.
selemet, Это уже тебе решать. Я думаю, что не совсем удобно будет.
.
Сексуальность валенка
reaper, Настройка геморная как по мне, я хотел недавно попробывать вникнуть в создание сайта на питоне, но для обычного привет мир мне потребовалось ставить какие то фреймворки настраивать это все и т.д. короче кинул эту затею, из за этого наверно питон не так масово используют как php(
.
Swank, Можно и без фреймворка написать какой-нибудь hello world, но когда будешь разрабатывать реальный проект, то так же, как и в пыхе лучше взять какой-нибудь фреймворк, потому что это позволяет сразу начать решать конкретные задачи, а не писать очередной велосипед.
А т.к. в питоне приложение запускается один раз, нужно будет решать кучу проблем с состоянием приложения, которые уже решены в существующих фреймворках.
Можно использовать его и как cgi, но это далеко не лучшее решение.
.
Swank
Сексуальность валенка
reaper, Я не переварюю фреймворки, потому что эсли я собирабсь чтото писать то перед этим я должен знать как все работает, а в фреймворка тяжолый код для меня, и я не понимаю как там все работает, изза этого я не уверен в производительности т.к. я не знаю как работает часть моего сайта и у меня вообще пропадает желения писать, я вообще если что то делаю то мне обязательно надо знать как это работает, например после установки линя я сразу начал изучать си потом немного асамблер, архитектуры процесоров, модули процесоров и все ниже и ниже
Поэтому я не люблю фреймворки и изучать не собираюсь т.к. их надо изучать постоянно, сегодне один актуален завтро другой...
Лучше изобрести свой велосипед в котором ты будеш уверен, знать его и он будет оптимизирован именно под твои нужды, а не тысячи людей
Всего: 18