11 мая 2015 г.

Digital Ocean для Continuous Integration! Gitlab + Jenkins [tutorial].

В седые времена на настройку команды разработчиков уходило много времени и сил. Мало было просто поселить в голову отдельного разработчика идеи о TDD и его пользе. Ещё нужны были администраторы с руками растущими из правильных мест (ну это те, у которых кофта заправлена в джинсы и на лице густая борода), и нужно они были не один разок, а постоянно. Сейчас все иначе. Имея базовые знания можно, развернуть весь continuous integration (CI) не открываясь от смузи.

Итак, нам потребуется: $10 в месяц на оплату собственного VPS (на нем же можно хостить MVP вашего стартапа, и еще много всякого - например VPN-шлюз для маскировки под американскую компанию), 2 опсосных проекта и некоторое количество свободного времени.

Gitlab - это такой открытый Github у вас на сервере. Можно конечно обойтись Bitbucket'ом с закрытой группой (team), но это во первых не круто =), а во-вторых, стартап должен параноидально охранять свою супер новую уникальную социальную сеть "Водноклассников Мир".

Jenkins - инструмент непрерывной интеграции, написанный на Java. Есть много разных альтернатив, но вот что нужно запомнить про них про все (субъективно):
  • если инструмент имеет продвинутый функционал - он написан на java
  • если инструмент написан не на java, скорее всего из коробки он умеет очень мало, а настройка всего дополнительного - боль
  • настройка любых метрик в любой CI - это тоже боль
  • каждая команда имеет свой совершенно уникальный процесс разработки, нет двух одинаковых CI-процессов
  • каждая команда стремится создать свой уникальный deploy-процесс, но автоматизировать нужно только то, в чем админы реально не принимают никакого участия (читай: и не несут никакой ответственности). Выкладка новой версии внутренней python-библиотеки в закрытый репозиторий компании - хороший пример такой работы
  • и самое главное: невозможно один раз настроить CI и никогда больше к нему не возвращаться. Поддержание работоспособности CI - это отдельная настоящая работа, которой тоже нужно заниматься. Постоянно. Правда, всё потраченное время и силы вернутся сторицей.
Digital Ocean (DO) - очень модный продавец VPS'ок, который держит нос исключительно "по ветру". Цены у них может быть и не самые низкие, но: API, хранение и дистрибуция слепков, куча дата-центров по всему миру, и настройка в один клик - безусловно доставляют.

Теперь о том, как все это соединить без напрягов.

1. Создаем сервер с GitLab на борту при помощи one-click-install
Не вижу никакого смысла повторять или переводить туториал написанный DO: How To Use the GitLab One-Click Install Image to Manage Git Repositories.
DO не рекомендует устанавливать Gitlab на дроплет за 5 долларов - слишком уж мало там памяти. $10 - будет вполне достаточно. Но если очень хочется сэкономить, можно просто увеличить swap - для машинки с такими утилитарными задачами LA не критичен, как по мне. При заходе на one-click install с регистрации вам не дадут использовать $5 дроплет для gitlab, но если зарегистрироваться и уже из кабинета начать создавать дроплет (точно так, как описано в tutorial выше), то такого ограничения нет.

2. Устанавливаем Jenkins.
Для этого случая, что не удивительно, у Digital Ocean Community тоже уже есть tutorial: https://www.digitalocean.com/community/tutorials/how-to-install-and-use-jenkins-on-ubuntu-12-04. От версии ubuntu в этом руководстве вообще ничего не зависит, так что смело используйте и для 14ой убунту. Заходите на VPS по ssh и далее все как в tutorial.
По умолчанию Jenkins попытается использовать порт 8080, который уже занял gitlab. Нужно в файле /etc/default/jenkins заменить этот порт на какой-нибудь другой. Например, 9090.

Далее, наверняка захочется настроить для jenkins какое-нибудь приятное доменное имя, например ci.v-odnoklassnikov-moi-mir.ru. Для этого нам вполне хватит nginx, который установился вместе с gitlab. Его настройки лежат в каталоге /var/opt/gitlab/nginx/conf/.
Создадим файл /var/opt/gitlab/nginx/conf/jenkins.conf с содержимым из туториала jenkins: Jenkins behind an NGinX reverse proxy. Обратите внимание, что в нашем случае proxy_pass смотрит на "http://localhost:9090/;".
Только что написанный конфиг нужно "подцепить" в nginx GitLab'а. Данный процесс описан тут. Суть статьи по ссылке: нужно в файле /etc/gitlab/gitlab.rb, добавить строку:
nginx['custom_nginx_config'] = "include /var/opt/gitlab/nginx/conf/jenkins.conf;"
Не уверен, что файл  /var/opt/gitlab/nginx/conf/jenkins.conf не похерится во время обновления gitlab. Будем посмотреть...

Вот и все. В этом посте осознанно не останавливаюсь на настройках проектов в Jenkins, чтобы не ограничиваться целями конкретного проекта / платформы. Я использую дженкинс для авто-тестов python-проектов, но в нем есть все для тестирования мобильных приложений и еще много-много всякого.
Думаю, полезно было бы рассказать как при запросах на мердж jenkins умеет создавать отдельные проекты и тесты, как здорово использовать хуки gitlab'а, чтобы всегда иметь актуальную картину тестов, но все этом потом.

P. S. В комментах к посту помимо "<все не так>" и "<я тоже умный>" прошу написать: что еще по теме поста интересно лично тебе, мой дорогой подписчик?

P. P. S. Лучшей благодарностью будет зарегистрироваться в Digital Ocean по моей реферальной ссылке https://www.digitalocean.com/?refcode=e16041a7fe90. Вам не сложно, а мне приятно.

4 мая 2015 г.

Django-jenkins и COVERAGE_EXCLUDES_FOLDERS

Возился минут 10 пытаясь подобрать правильный способ прописать каталоги исключенные из coverage отчета для jenkins, залез в код и выяснил, что правильно определять полный путь до каталога, а не как в обсуждении на stackoverflow. Пора бы уже привыкнуть лезть в код раньше, чем на стэк =).
Вот кусок конфига для django + django-jenkins с покрытием:
import os


BASE_DIR = os.path.dirname(os.path.dirname(__file__))

# ...

PROJECT_APPS = ('myapp', )

JENKINS_TASKS = (
    'django_jenkins.tasks.run_pylint',
    'django_jenkins.tasks.run_pep8',
    'django_jenkins.tasks.run_pyflakes',
)

COVERAGE_EXCLUDES_FOLDERS = [
    os.path.join(BASE_DIR, 'myapp', 'tests')
]

Запускаются такие тесты так:
python manage.py jenkins --enable-coverage

14 апр. 2015 г.

Moscow Django MeetUp

Не мало воды утекло с тех пор, как я посещал последний раз это мероприятие. С удовольствием констатирую, что качество докладов сильно выросло: Moscow Django MeetUp № 26.

5 мар. 2015 г.

Интересный способ привлечь внимание пользователей bitbucket

Сегодня ночью мне пришло письмо от bitbucket, о том что не знакомый мне человек дает мне грант на запись в репозиторий. В названии репозитория доменное имя, по которому замещен интернет-магазин.
Грант, очевидно, был отозван сразу. Мне естественно, стало интересно посмотреть что там на сайте. Тему этого сайта для себя считаю закрытой, но какая-то более специализированная история, могла бы меня заинтересовать. Например, так можно раскручивать сервис непрерывной интеграции или аутсорс тестирования.
Для того что бы выдать грант пользователю достаточно знать его ФИ или username. Такие данные не сложно получить с помощью API bitbucket'а http://restbrowser.bitbucket.org/.

24 февр. 2015 г.

Timeout декоратор

Если функция будет работать долго, и при этом нужно чтобы она работала не дольше определенного количества секунд, можно применить такой вот декоратор:

import errno
import os
import signal
from functools import wraps


class TimeoutError(Exception):
    pass


def timeout(seconds=10, error_message=os.strerror(errno.ETIME)):
    """
    @timeout(60)
    def long_functions():
        <your code here>
    """
    def decorator(func):
        def _handle_timeout(signum, frame):
            raise TimeoutError(error_message)

        def wrapper(*args, **kwargs):
            signal.signal(signal.SIGALRM, _handle_timeout)
            signal.alarm(seconds)
            try:
                result = func(*args, **kwargs)
            finally:
                signal.alarm(0)
            return result

        return wraps(func)(wrapper)

    return decorator

Lock в файл или в redis?

Иногда нужно гарантировать эксклюзивное исполнение функции для определенного набора данных. Например, только одно одновременное изъятие данных из какого-либо источника. На помощь в таких ситуациях приходят локи.

Проще всего создать какой-нибудь lock-файл и проверять его существование. Если есть файл - lock включен.
import os
from contextlib import contextmanager


class LockedException(Exception):
    pass


@contextmanager
def file_lock(lock_file):
    """
    with file_lock('/tmp/file.lock'):
        some code here
    """
    if os.path.exists(lock_file):
        raise LockedException('LockedException')
    else:
        open(lock_file, 'w').write("1")
        try:
            yield
        finally:
            os.remove(lock_file)

Жалко напрягать HDD такими мелкими операциями. Попробуем делать lock в redis:
import redis
from contextlib import contextmanager


r = r = redis.StrictRedis(host='localhost', port=6379, db=0)


class LockedException(Exception):
    pass

@contextmanager
def redis_lock(lock_id):
    """
    with redis_lock('unique_redis_key'):
        some code here
    """
    if r.get(lock_id):
        raise LockedException('LockedException')
    else:
        r.set(lock_id, 1)
        try:
            yield
        finally:
            r.delete(lock_id)

Теперь проверим какой вариант быстрей:
if __name__ == '__main__':
    import time
    from numpy import mean


    count = 10000

    times1 = []
    for i in xrange(0, count):
        start = time.time()
        with file_lock('/tmp/file.lock'):
            pass

        times1.append(float(time.time() - start))

    times2 = []
    for i in xrange(0, count):
        start = time.time()
        with redis_lock('redis_lock'):
            pass

        times2.append(float(time.time() - start))

    print 'file lock:', max(times1), min(times1), mean(times1)
    print 'redis lock:', max(times2), min(times2), mean(times2)

Результаты:

maxminavg
file_lock0.08177495002750.000284910202026 0.000996773791313
redis_lock0.00140619277954 0.000319957733154 0.000346991157532

Lock в redis в 3 раза быстрей.

12 янв. 2015 г.

Shiningpanda и jenkins

Надкусил собаку, при настройке CI на базе jenkins. Проглотить мешают постоянные всякие мелочи. Например из последнего: плагин Virtualenv Builder [Shiningpanda], с помощью которого можно более лучше развертывать виртуальные окружения python, принимает настройку "name". По документации, настройка соответствует названию virtualenv окружения. Изменение этой опции не влияет на фактическое название окружения (по факту получается что-то вроде c934fdf).
При всем этом, Shiningpanda реально экономит время при создании CI для python проекта.

Сборка:
[Virtualenv Builder]
pip install pytest pytest-cov
$PYTHON_EXE setup.py install
py.test --junitxml junit.xml --cov-report xml --cov src tests.py

Постсборка:
[Cobertura coverage]
coverage.xml

[juint]
result.xml

И далее в постсборке запускает выкатка библиотеки и/или обновление девелоперского-стейджа.

А как в ваших компаниях реализован CI?