Безперервна інтеграція.
Основи роботи з Linux. -> Сервер безперервної інтеграції на python.
Сервер безперервної інтеграції на python.
Безперервна інтеграція включає процес застосування змін коду після внесення їх через git на серверах.
Тобто. після того, як розробник вніс зміни та залив їх у git, система в автоматичному режимі виробляє деплой коду на тестовий сервер, проводить усі необхідні операції зі складання та тестування, а потім переносить усе на бойовий.
Для автоматизації таких процесів є такі інструменти як Jenkins.
Він є окремим сервером з безліччю функцій і навіть своєю мовою для опису сценаріїв. Він здатний обслуговувати відразу безліч проектів, але досить складний у вивченні.
Пропоную спочатку побудувати свій власний сервер інтеграції на основі Django і “заточити” його під один єдиний проект.
Завдання сервера полягатиме в наступному:
Користувачеві пропонується ввести email та натиснути на кнопку “Створити робочу область”.
Система на сервері робить таке:
-
Створює на диску каталог із ім’ям наданого emeil-a.
-
Клонує репозиторій проекту на Django (у нашому випадку).
3.Створює і пушить новий бранч у git на ім’я email-а.
-
Встановлює всі залежності проекту.
-
Створює конфіги де:
-
Підключає проект до БД.
-
створює конфіг для nginx, в якому описує новий піддомен, спрямований на новий репозиторій
-
підключає репозиторій до папки media (одна для всіх оскільки вона велика)
-
надає доступ до папки проекту
Нам знадобиться налаштувати доменну зону і вказати в записі А, що ми хочемо мати довільну кількість піддоменів.
Створимо стартовий проект Django з ім’ям ci.
django-admin startproject ci
Файл залежностей requirements.txt
Django
Скрипт установки install
python3 -m venv venv
. ./venv/bin/activate
pip3 install -r requirements.txt
cd ci
./manage.py migrate
Створюємо додаток
./manage.py startapp main
Включаємо його у налаштуваннях.
Головний сторінки шаблону.
<html lang="en">
<!-- Basic -->
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title> CI </title>
</head>
<body>
<h1> Создать рабочую область </h1>
<form method="POST" action="" >
{% csrf_token %}
Email
<input type="text" name="email" />
<button>Создать</button>
</form>
</body>
</html>
Нам доведеться десь зберігати змінні оточення.
Для цього створимо файл .env
Визначаємо папку, де створюватимемо оточення та репозиторій, з якого клонуватимемо проекти в оточення.
WORK_DIR=/home/zdimon/tmp
GIT_URL=git@github.com:zdimon/pressa-besa.git
Встановимо пакет для читання файлів такого типу.
pip install python-dotenv
І пропишемо в settings.py
from dotenv import load_dotenv
load_dotenv()
WORK_DIR = os.getenv('WORK_DIR')
GIT_URL = os.getenv('GIT_URL')
Створимо модель і застосуємо міграцію.
from django.db import models
class Env(models.Model):
email = models.CharField(max_length=60,unique=True)
port = models.IntegerField(default=8080,unique=True)
Адмінка.
from django.contrib import admin
from .models import Env
@admin.register(Env)
class EnvAdmin(admin.ModelAdmin):
list_display = ['email','port']
Створимо клас форми у файлі forms.py
from django.forms import ModelForm
from .models import Env
class EnvForm(ModelForm):
class Meta:
model = Env
fields = ['email']
Виводимо у шаблоні.
<form method="POST" action="" >
{% csrf_token %}
{{ form }}
<button>Создать</button>
</form>
Відпрацюємо сабміт.
from django.shortcuts import render
from .forms import EnvForm
def index(request):
if request.method == 'POST':
form = EnvForm(request.POST)
if form.is_valid():
form.save()
else:
form = EnvForm()
return render(request,'index.html', {"form": form})
Результат
Реалізуємо моделі додавання номера порту для кожного нового запису.
from django.db import models
from django.db.models.signals import post_save
from django.db.models import Max
class Env(models.Model):
email = models.CharField(max_length=60,unique=True)
port = models.IntegerField(default=8080)
@classmethod
def post_create(cls, sender, instance, created, *args, **kwargs):
if created:
maxp = Env.objects.aggregate(Max('port'))
print(maxp)
instance.port = maxp["port__max"]+1
instance.save()
post_save.connect(Env.post_create, sender=Env)
У файлі tasks.py створимо функцію створення каталогу.
from django.conf import settings
import os
def create_dir(env):
login = env.email.replace('@','---')
path = os.path.join(settings.WORK_DIR,login)
os.mkdir(path)
Викличемо її під час створення запису.
def post_create(cls, sender, instance, created, *args, **kwargs):
if created:
...
create_dir(instance)
Встановимо бібліотеку для гіта
pip install GitPython
Клонування репозиторію.
import git
def git_clone(env)
path = os.path.join(settings.WORK_DIR,env.email.replace('@','---'))
git.Git(path).clone(settings.GIT_URL)
Але ми стикаємося з проблемою часу виконання т.к. процес може бути довгим і користувача не варто змушувати чекати довгого завантаження сторінки.
Для цього придумали відкладені завдання (типу отримай ЩАС html сторінку з OK, а ми в роботі, і потім буде вихлоп).
Вирішуємо їх за допомогою celery (основний гравець на полі python).
Встановлення.
pip install redis
pip install celery
Необхідно створити програму celery у новому файлі celery_app.py поряд з settings.py.
from celery import Celery
from django.conf import settings
import os
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ci.settings')
app = Celery()
app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)
В settings.py установить Redis як брокер повідомлень.
REDIS_HOST = os.getenv('SQL_HOST', 'redis-server')
REDIS_PORT = os.getenv('SQL_PORT', '6379')
CELERY_BROKER_URL = 'redis://' + REDIS_HOST + ':' + str(REDIS_PORT)
Додати імпорт до init.py
from .celery_app import app as celery_app
Запуск вокера.
celery -A ci worker -l info
ci - ім’я проекту
Задекоруємо функції.
from django.conf import settings
import os
import git
from celery.decorators import task
@task()
def create_dir(env):
login = env.email.replace('@','---')
path = os.path.join(settings.WORK_DIR,login)
os.mkdir(path)
@task()
def git_clone(env):
path = os.path.join(settings.WORK_DIR,env.email.replace('@','---'))
git.Git(path).clone(settings.GIT_URL)
Передамо на виконання вокеру селери.
create_dir.delay(instance)
git_clone.delay(instance)
Передача об’єкта не працює, тому передамо id.
git_clone.delay(instance.id)
І виберемо із бази об’єкт.
@task()
def git_clone(env_id):
from .models import Env
env = Env.objects.get(pk=env_id)
path = os.path.join(settings.WORK_DIR,env.email.replace('@','---'))
git.Git(path).clone(settings.GIT_URL)
from .models import Env - імпорт проводимо всередині функції, щоб уникнути циклічного імпорту.
Конфіг nginx
Додаємо додаткові змінні до .env
NGINX_PATH=/etc/nginx/sites-enabled
DOMAIN=local
MEDIA_PATH=/home/zdimon/Desktop/work/pressa-besa/media/
PROJECT_PATH=pressa-besa
DB_PATH=/home/zdimon/Desktop/work/pressa-besa/backend/db.sqlite3
Прописуємо у settings.
DOMAIN = os.getenv('DOMAIN')
NGINX_PATH = os.getenv('NGINX_PATH')
MEDIA_PATH = os.getenv('MEDIA_PATH')
PROJECT_PATH = os.getenv('PROJECT_PATH')
DB_PATH = os.getenv('DB_PATH')
Перш ніж писати в системний каталог, створимо в ньому підкаталог і встановимо права на запис.
sudo chown $UID:$GID /etc/nginx/sites-enabled
Також, якщо ви хочете, щоб робочий каталог знаходився в домашній директорії окремого користувача, необхідно дати права на запис тому користувачеві від імені якого стартує система.
Наприклад, якщо сервер інтеграції стартує від імені користувача pressa, а працювати повинні від імені користувача dev
sudo setfacl -m u:pressa:rwx /home/dev
Даємо права dev для запису в директорії, створені від імені pressa, додаючи його до групи pressa
sudo usermod -a -G pressa dev
Функція запису конфігу nginx.
def nginx_conf(env_id):
from .models import Env
env = Env.objects.get(pk=env_id)
path = os.path.join(settings.BASE_DIR, 'tpl', 'nginx_vhost.conf')
with open(path, 'r') as f:
tpl = f.read()
tpl = tpl.replace('%media_path%', settings.MEDIA_PATH)
sname = '%s.%s' % (env.email.replace('@', '---'), settings.DOMAIN)
tpl = tpl.replace('%server_name%', sname)
tpl = tpl.replace('%port%', str(env.port))
conf_path = os.path.join(
settings.NGINX_PATH, env.email.replace('@', '---'))
with open(conf_path, 'w+') as f:
f.write(tpl)
Конфіг supervisor
Встановлюємо супервізор
sudo apt install supervisor
Дивимося шлях яким він инклудит конфи
sudo nano /etc/supervisor/supervisord.conf
[include]
files = /etc/supervisor/conf.d/*.conf
Змінимо шлях.
[include]
files = /home/zdimon/Desktop/work/pressa-besa/ci/configs/supervisor/*.conf
Перевантажимо
sudo service supervisor restart
Отримуємо помилку
Значить запускати будемо через sudo
Перегляд завдань
supervisorctl
Пробуємо тестовий сервер у файлі test.conf.
[program:test]
user = zdimon
directory = /home/zdimon/Desktop/work/pressa-besa/ci/ci
command = python3 manage.py runserver 0.0.0.0:9898
autostart = true
autorestart = true
Пробуємо додати оточення та лог помилок.
environment=PYTHONPATH="/home/zdimon/Desktop/work/pressa-besa/ci/venv"
stderr_logfile=/home/zdimon/Desktop/work/pressa-besa/ci/logs/error.log
Можемо спостерігати у лозі
ModuleNotFoundError: No module named 'celery'
Схоже, що оточення не підключається.
Пробую застосувати бінарник інтерпретатора.
command = /home/zdimon/Desktop/work/pressa-besa/ci/venv/bin/python3 manage.py runserver 0.0.0.0:9898
Тепер усе працює.
Створюємо шаблон під конфіг
[program:%name%]
directory = %prj_dir%
command = %env_dir%/bin/python3 manage.py runserver 0.0.0.0:%port%
autostart = true
autorestart = true
environment=PYTHONPATH="%env_dir%"
stderr_logfile=/home/zdimon/Desktop/work/pressa-besa/ci/logs/%name%.log
Оформлюємо функцію під конфіг.
def supervisor_conf(env_id):
from .models import Env
env = Env.objects.get(pk=env_id)
path = os.path.join(settings.BASE_DIR, 'tpl', 'supervisor.conf')
with open(path, 'r') as f:
tpl = f.read()
sname = '%s.%s' % (env.email.replace('@', '---'), settings.DOMAIN)
tpl = tpl.replace('%name%', sname)
tpl = tpl.replace('%port%', str(env.port))
prj_dir = os.path.join(settings.WORK_DIR, env.email.replace(
'@', '---'), settings.PROJECT_PATH)
tpl = tpl.replace('%prj_dir%', prj_dir)
tpl = tpl.replace('%env_dir%', settings.ENV_PATH)
filename = '%s.conf' % env.email.replace('@', '---')
conf_path = os.path.join(
settings.BASE_DIR, 'configs', 'supervisor', filename)
with open(conf_path, 'w+') as f:
f.write(tpl)
Залишилося зробити рестарт supervisor-а та nginx-а.
bashCommand = "sudo service supervisor restart"
process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
print(error)
Отримали запит пароля у вікні з селера.
Значить будемо запускати з-під рута.
sudo celery -A ci worker -l info
Тоді відвалився git т.к. у git не додані ключі суперкористувача.
Please make sure you have the correct access rights
and the repository exists.'
Пробуємо придушити запит пароля для sudo
sudo visudo
Вставляємо рядок.
zdimon ALL=(ALL) NOPASSWD: ALL
Видалення робочої області.
Створимо завдання для celery, яка видалятиме всю область при видаленні користувача з моделі Env.
@task()
def clear_env(email):
import subprocess
# remove env path
env_path = os.path.join(settings.WORK_DIR, email)
bashCommand = "sudo rm -r %s" % env_path
process = subprocess.Popen(bashCommand.split(), stdout=subprocess.PIPE)
output, error = process.communicate()
print(error)
# remove nginx conf
nginx_path = os.path.join(
settings.NGINX_PATH, email)
os.remove(nginx_path)
# remove supervisor conf
filename = '%s.conf' % email
supervisor_conf_path = os.path.join(
settings.BASE_DIR, 'configs', 'supervisor', filename)
os.remove(supervisor_conf_path)
У це завдання ми передаватимемо email т.к. на момент її виклику запису в базі може вже не бути і ми не зможемо її вибрати по id.
Викликаємо завдання.
from django.db.models.signals import pre_delete
def pre_delete_handler(sender, instance, using, **kwargs):
clear_env.delay(normalize_email(instance.email))
pre_delete.connect(pre_delete_handler, sender=Env)