Пишем простой buildserver на Python (Часть 1)
от Screamer
В этой статье я расскажу о том, как написать простой 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
Создаём файл buildout.cfg:
$ editor buildout.cfg
Создаём пакет 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 пишем:
Здесь мы определили путь к директорию, куда будут установлены зависимости, описанные в bower.json
В bower.json:
Ну здесь всё понятно, а что не понятно, смотрите доку на офф сайте.
Выполняем:
$ 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
Выполняем:
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/
Вся работа сводится к созданию подключения, получению курсора, выполнению запроса и коммита транзакции.
Создаём файл src/buildserver/app/repositories.py
В нём будут располагаться классы, необходимые для работы с БД.
Чтобы выполнять команды в шелле из питона, воспользуемся модулем subprocess.
Почитать о нём на русском языке можно здесь.
Если вы читали материал, приведённый по ссылке выше, то никаких вопросов возникнуть не должно.
Настройки приложения будут располагаться в src/buildserver/app/settings.py:
Создаём директории logs и builds в корне проекта.
Было бы неплохо иметь централизованный доступ к логам сборок.
Теперь, чтобы можно было импортировать только что созданные модули, необходимо сказать питону, что 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> - Получить информацию о сборке
Переходим к реализации.
REST API готово. Переходим к воркеру.
Но для начала давайте немного поиграемся с pyzmq.
Создадим в корне проекта два файла producer.py и publisher.py
producer:
publisher:
Запускаем:
$ bin/py publisher.py
$ bin/py producer.py
На выходе получим:
Круто, не правда ли?
Удаляем publisher.py и producer.py, они нам больше не пригодятся.
Создаём воркер.
Теперь необходимо сделать отображение лога сборки в режиме реального времени.
Ну вот, как-то так.
Остаётся сделать фронтенд и научиться запускать всё это дело.
Для начала хорошо было бы знать, что такое 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
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...l#DDL-CONSTRAINTS-FK
Большинство пакетов для работы с базой данных реализуют Database API Specification 2.0
https://www.python.org/dev/peps/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
Ну вот, как-то так.
Остаётся сделать фронтенд и научиться запускать всё это дело.