Ez a cikk mind a tesztelők, mind a fejlesztők számára érdekes lesz, de elsősorban az automatizálóknak szól, akik szembesültek a GitLab CI/CD beállításának problémájával integrált tesztelés végrehajtásához, ha az infrastruktúra erőforrásai nem elegendőek és/vagy hiányzik a konténerorchestration platform. Megmutatom, hogyan lehet beállítani a tesztelt környezetek telepítését docker compose segítségével egyetlen GitLab shell runneren, úgy, hogy a telepítés során a futó szolgáltatások ne zavarják egymást.
Tartalom
Előzmények
A gyakorlatomban gyakran előfordult, hogy a projektek integrációs tesztelését "kezeltem". És sok esetben az első és legjelentősebb probléma a CI pipeline, ahol az integrációs tesztelés a fejlesztett szolgáltatás(ok) dev/stage környezetben történik. Ez számos problémát okozott:
- A tesztelt környezet a hibás adatok miatt tönkremehet egy vagy másik szolgáltatás hibái miatt az integrációs tesztelés során. Volt olyan eset, amikor a hibás JSON formátumú kérés küldése leállította a szolgáltatást, ami teljesen működésképtelen állapotba juttatta a standot.
- A tesztkörnyezet teljesítménycsökkenése a tesztadatok mennyiségének növekedésével. Gondolom, nincs értelme példát említeni az adatbázis tisztításának/visszaállításának folyamatáról. A gyakorlatomban nem találkoztam olyan projekttel, ahol ez a folyamat simán ment volna.
- A tesztkörnyezet működésének veszélye, ha a rendszer közös beállításait teszteljük. Például, felhasználó/csoport/jelszó/alapértelmezett alkalmazás politika.
- A tesztadatok az automatizált tesztekből zavarják a manuális tesztelőket.
Valaki azt mondhatja, hogy a jó automatizált teszteknek tisztítaniuk kell az utánuk maradt adatokat. Nekem vannak érveim ezzel szemben:
- A dinamikus standok rendkívül kényelmesek.
- Nem minden objektumot lehet törölni a rendszerből API-n keresztül. Például az objektum törlésére vonatkozó hívás nem valósult meg, mivel ellentétes az üzleti logikával.
- API-n keresztüli objektum létrehozásakor hatalmas mennyiségű metaadatok keletkezhetnek, amelyeket nehéz törölni.
- Ha a tesztek között összefüggések vannak, akkor az adatok tisztítása a tesztek végrehajtása után igazi fejfájássá válik.
- További (és véleményem szerint indokolatlan) API-hívások.
- És a legfontosabb érv: amikor a tesztadatokat közvetlenül az adatbázisból kezdik el tisztítani. Ez igazi PK/FK cirkuszba torkollik! A fejlesztőktől hallani: „Csak egy táblát adtam hozzá/ töröltem/ átneveztem, miért futott 100500 integrációs teszt?”
Szerintem a legoptimálisabb megoldás a dinamikus környezet.
- Sokan használnak docker-compose-t a tesztkörnyezet elindításához, de kevesen alkalmazzák a docker-compose-t az integrációs tesztelés során a CI/CD-ben. Ekkor nem számítok bele a kubernetes-t, swarmot és más konténerkezelő platformokat. Nem minden cégnél vannak jelen. Jó lenne, ha a docker-compose.yml univerzális lenne.
- Még ha van is saját QA futtatónk, hogyan biztosítsuk, hogy a docker-compose által elindított szolgáltatások ne zavarják egymást?
- Hogyan gyűjtsük a tesztelt szolgáltatások naplóit?
- Hogyan tisztítsuk a futtatót?
Van egy saját GitLab futtatóm a projektjeimhez, és ezekkel a kérdésekkel találkoztam a fejlesztés során. a . Vagyis az integrációs tesztek futtatásakor. Most pedig ezeket a kérdéseket fogjuk megoldani a projekt példáival.
GitLab Shell Runner
Futónak linuxos virtuális gépet ajánlok 4 vCPU-val, 4 GB RAM-mal, 50 GB HDD-vel.
Az interneten nagyon sok információ található a gitlab-runner beállításáról, ezért röviden:
- Jelentkezz be a gépre SSH-n.
Ha kevesebb mint 8 GB RAM-od van, akkor ajánlom, , hogy ne aktiválódjon az OOM killer és ne ölje meg a feladatainkat a RAM hiánya miatt. Ez előfordulhat, ha egyszerre több mint 5 feladatot indítanak. A feladatok lassabban futnak, de stabilan.
Példa az OOM killer-re.
Ha a feladat naplóiban ezt látod:
bash: line 82: 26474 Killed, akkor egyszerűen futtasd a futtatón:sudo dmesg | grep 26474[26474] 1002 26474 1061935 123806 339 0 0 java Out of memory: Kill process 26474 (java) score 127 or sacrifice child Killed process 26474 (java) total-vm:4247740kB, anon-rss:495224kB, file-rss:0kB, shmem-rss:0kBÉs ha a kép körülbelül így néz ki, akkor vagy adj hozzá swap-ot, vagy növeld a RAM-ot.
- Telepítünk , , , make.
- Felhasználót adunk hozzá
gitlab-runner-t használtama csoporthoz:dockersudo groupadd docker sudo usermod -aG docker gitlab-runner - a gitlab-runnert.
Megnyitjuk szerkesztésre
/etc/gitlab-runner/config.tomlés hozzáadjuk:concurrent=20 [[runners]] request_concurrency = 10Ez lehetővé teszi párhuzamos feladatok futtatását egy futtatón. További részletek .
Ha erősebb géped van, például 8 vCPU-ral és 16 GB RAM-mal, akkor ezek a számok legalább 2-szer nagyobbak lehetnek. De minden attól függ, hogy pontosan mi fog futni ezen a futtatón, és milyen mennyiségben.
Ez elegendő.
docker-compose.yml előkészítése
A fő feladat egy univerzális docker-compose.yml készítése, amelyet a fejlesztők/teszterek helyben és a CI folyamatban is használhatnak.
Elsősorban egyedi szolgáltatásneveket hozunk létre a CI számára. Az egyik egyedi változó a GitLab CI-ben a változó CI_JOB_ID. Ha megadod a container_name az értékkel "service-${CI_JOB_ID:-local}", akkor az esetén:
- ha
CI_JOB_IDnem definiált a környezeti változókban,
akkor a szolgáltatás neveservice-local - ha
CI_JOB_IDdefiniált a környezeti változókban (például 123),
akkor a szolgáltatás neveservice-123
Másodsorban közös hálózatot készítünk a futó szolgáltatások számára. Ez hálózati szintű izolációt biztosít több tesztkörnyezet futtatásakor.
networks:
default:
external:
name: service-network-${CI_JOB_ID:-local}Ez valóban az első lépés a siker felé =)
Példa az én docker-compose.yml fájlomra megjegyzésekkel
version: "3"
# A web (php) és fmt megfelelő működéséhez
# a konténereknek közös végrehajtható tartalommal kell rendelkezniük.
# A mi esetünkben ez a könyvtár /var/www/testrail
volumes:
static-content:
# Izoláljuk a környezetet hálózati szinten
networks:
default:
external:
name: testrail-network-${CI_JOB_ID:-local}
services:
db:
image: mysql:5.7.22
# Minden container_name tartalmazza a ${CI_JOB_ID:-local}-t
container_name: "testrail-mysql-${CI_JOB_ID:-local}"
environment:
MYSQL_HOST: db
MYSQL_DATABASE: mydb
MYSQL_ROOT_PASSWORD: 1234
SKIP_GRANT_TABLES: 1
SKIP_NETWORKING: 1
SERVICE_TAGS: dev
SERVICE_NAME: mysql
networks:
- default
migration:
image: registry.gitlab.com/touchbit/image/testrail/migration:latest
container_name: "testrail-migration-${CI_JOB_ID:-local}"
links:
- db
depends_on:
- db
networks:
- default
fpm:
image: registry.gitlab.com/touchbit/image/testrail/fpm:latest
container_name: "testrail-fpm-${CI_JOB_ID:-local}"
volumes:
- static-content:/var/www/testrail
links:
- db
networks:
- default
web:
image: registry.gitlab.com/touchbit/image/testrail/web:latest
container_name: "testrail-web-${CI_JOB_ID:-local}"
# Ha a TR_HTTP_PORT vagy a TR_HTTPS_PORTS változók nincsenek definiálva,
# akkor a szolgáltatás a 80-as és 443-as porton fut.
ports:
- ${TR_HTTP_PORT:-80}:80
- ${TR_HTTPS_PORT:-443}:443
volumes:
- static-content:/var/www/testrail
links:
- db
- fpm
networks:
- defaultPélda a helyi futtatásra
docker-compose -f docker-compose.yml up -d
Indítás testrail-mysql-local ... kész
Indítás testrail-migration-local ... kész
Indítás testrail-fpm-local ... kész
Újra létrehozás testrail-web-local ... készDe nem minden olyan egyszerű a CI-ben történő futtatással.
Makefile előkészítése
Használok Makefile-t, mivel ez rendkívül kényelmes helyi környezetkezeléshez, valamint CI-ben is. A következőkben inline megjegyzések.
# У меня в проектах все вспомогательные вещи лежат в директории `.indirect`,
# в том числе и `docker-compose.yml`
# Использовать bash с опцией pipefail
# pipefail - фейлит выполнение пайпа, если команда выполнилась с ошибкой
SHELL=/bin/bash -o pipefail
# Останавливаем контейнеры и удаляем сеть
docker-kill:
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml kill
docker network rm network-$${CI_JOB_ID:-testrail} || true
# Предварительно выполняем docker-kill
docker-up: docker-kill
# Создаем сеть для окружения
docker network create network-$${CI_JOB_ID:-testrail}
# Забираем последние образы из docker-registry
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml pull
# Запускаем окружение
# force-recreate - принудительное пересоздание контейнеров
# renew-anon-volumes - не использовать volumes предыдущих контейнеров
docker-compose -f $${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
# Ну и, на всякий случай, вывести что там у нас в принципе запущено на машинке
docker ps
# Коллектим логи сервисов
docker-logs:
mkdir ./logs || true
docker logs testrail-web-$${CI_JOB_ID:-local} >& logs/testrail-web.log
docker logs testrail-fpm-$${CI_JOB_ID:-local} >& logs/testrail-fpm.log
docker logs testrail-migration-$${CI_JOB_ID:-local} >& logs/testrail-migration.log
docker logs testrail-mysql-$${CI_JOB_ID:-local} >& logs/testrail-mysql.log
# Очистка раннера
docker-clean:
@echo Останавливаем все testrail-контейнеры
docker kill $$(docker ps --filter=name=testrail -q) || true
@echo Очистка докер контейнеров
docker rm -f $$(docker ps -a -f --filter=name=testrail status=exited -q) || true
@echo Очистка dangling образов
docker rmi -f $$(docker images -f "dangling=true" -q) || true
@echo Очистка testrail образов
docker rmi -f $$(docker images --filter=reference='registry.gitlab.com/touchbit/image/testrail/*' -q) || true
@echo Очистка всех неиспользуемых volume
docker volume rm -f $$(docker volume ls -q) || true
@echo Очистка всех testrail сетей
docker network rm $(docker network ls --filter=name=testrail -q) || true
docker ps
Ellenőrzés
make docker-up
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
Killing testrail-web-local ... kész
Killing testrail-fpm-local ... kész
Killing testrail-mysql-local ... kész
docker network rm network-${CI_JOB_ID:-testrail} || true
network-testrail
docker network create network-${CI_JOB_ID:-testrail}
d2ec063324081c8bbc1b08fd92242c2ea59d70cf4025fab8efcbc5c6360f083f
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Pulling db ... kész
Pulling migration ... kész
Pulling fpm ... kész
Pulling web ... kész
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
Recreating testrail-mysql-local ... kész
Recreating testrail-fpm-local ... kész
Recreating testrail-migration-local ... kész
Recreating testrail-web-local ... kész
docker ps
CONTAINER ID PORTS NAMES
a845d3cb0e5a 0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp testrail-web-local
19d8ef001398 9000/tcp testrail-fpm-local
e28840a2369c 3306/tcp, 33060/tcp testrail-migration-local
0e7900c23f37 3306/tcp testrail-mysql-local
make docker-logs
$ make docker-logs
mkdir ./logs || true
mkdir: cannot create directory ‘./logs’: A könyvtár már létezik
docker logs testrail-web-${CI_JOB_ID:-local} >& logs/testrail-web.log
docker logs testrail-fpm-${CI_JOB_ID:-local} >& logs/testrail-fpm.log
docker logs testrail-migration-${CI_JOB_ID:-local} >& logs/testrail-migration.log
docker logs testrail-mysql-${CI_JOB_ID:-local} >& logs/testrail-mysql.log
.gitlab-ci.yml előkészítése
Integrációs tesztek futtatása
Integráció:
szakasz: teszt
címkék:
- my-shell-runner
before_script:
# Hitelesítjük magunkat a registry-ben
- docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
# Létrehozzuk a pseudo-unikális TR_HTTP_PORT és TR_HTTPS_PORT értékeket
- export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
- export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
# Létrehozunk egy könyvtárat a feladat azonosítójával
- mkdir ${CI_JOB_ID}
# Átmásoljuk a létrehozott könyvtárba a docker-compose.yml fájlunkat
# hogy a kontextus különböző legyen minden feladat számára
- cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
script:
# Felemeljük a környezetünket
- make docker-up
# Futattunk teszteket az executálható jar fájl segítségével (én így csinálom)
- java -jar itest.jar --http-port ${TR_HTTP_PORT} --https-port ${TR_HTTPS_PORT}
# vagy egy konténerben
- docker run --network=testrail-network-${CI_JOB_ID:-local} --rm itest
after_script:
# Összegyűjtjük a naplókat
- make docker-logs
# Leállítjuk a környezetet
- make docker-kill
artifacts:
# Megőrizzük a naplókat
when: always
paths:
- logs
expire_in: 30 napEgy ilyen feladat futtatásának eredményeként a logs könyvtár az artefaktumok között a szolgáltatások és a tesztek naplóit tartalmazza. Ez nagyon kényelmes hibák esetén. Minden teszt párhuzamosan írja a saját naplóját, de erről külön fogok beszélni.

Runner tisztítása
A feladat csak ütemezés alapján fog futni.
szakaszok:
- tisztítás
- építés
- teszt
Tisztító futtató:
szakasz: tisztítás
csak:
- ütemezések
címkék:
- my-shell-runner
script:
- make docker-cleanTovább lépünk a GitLab projektünkbe -> CI/CD -> Ütemezések -> Új ütemezés és hozzáadunk egy új ütemtervet
Eredmény
Futtatunk 4 feladatot a GitLab CI-ben
Az utolsó feladat integrációs tesztjeinek naplóiban látjuk a különböző feladatok konténereit
KONTÉNER AZONOSÍTÓ NEVEK
c6b76f9135ed testrail-web-204645172
01d303262d8e testrail-fpm-204645172
2cdab1edbf6a testrail-migration-204645172
826aaf7c0a29 testrail-mysql-204645172
6dbb3fae0322 testrail-web-204645084
3540f8d448ce testrail-fpm-204645084
70fea72aa10d testrail-mysql-204645084
d8aa24b2892d testrail-web-204644881
6d4ccd910fad testrail-fpm-204644881
685d8023a3ec testrail-mysql-204644881
1cdfc692003a testrail-web-204644793
6f26dfb2683e testrail-fpm-204644793
029e16b26201 testrail-mysql-204644793
c10443222ac6 testrail-web-204567103
04339229397e testrail-fpm-204567103
6ae0accab28d testrail-mysql-204567103
b66b60d79e43 testrail-web-204553690
033b1f46afa9 testrail-fpm-204553690
a8879c5ef941 testrail-mysql-204553690
069954ba6010 testrail-web-204553539
ed6b17d911a5 testrail-fpm-204553539
1a1eed057ea0 testrail-mysql-204553539Részletesebb napló
$ docker login -u gitlab-ci-token -p ${CI_JOB_TOKEN} ${CI_REGISTRY}
FIGYELMEZTETÉS! A jelszó CLI-n történő használata nem biztonságos. Használja a --password-stdin-t.
FIGYELMEZTETÉS! A jelszava titkosítatlanul lesz tárolva a /home/gitlab-runner/.docker/config.json fájlban.
Állítson be egy hitelesítő segédet ennek a figyelmeztetésnek az eltávolításához. Lásd
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
A bejelentkezés sikeres
$ export TR_HTTP_PORT=$(shuf -i10000-60000 -n1)
$ export TR_HTTPS_PORT=$(shuf -i10000-60000 -n1)
$ mkdir ${CI_JOB_ID}
$ cp .indirect/docker-compose.yml ${CI_JOB_ID}/docker-compose.yml
$ make docker-up
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml kill
docker network rm testrail-network-${CI_JOB_ID:-local} || true
Hiba: Ilyen hálózat nem létezik: testrail-network-204645172
docker network create testrail-network-${CI_JOB_ID:-local}
0a59552b4464b8ab484de6ae5054f3d5752902910bacb0a7b5eca698766d0331
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml pull
Web lehúzása ... kész
FPM lehúzása ... kész
Migráció lehúzása ... kész
DB lehúzása ... kész
docker-compose -f ${CI_JOB_ID:-.indirect}/docker-compose.yml up --force-recreate --renew-anon-volumes -d
"204645172_static-content" volume létrehozása alapértelmezett meghajtóval
Testrail-mysql-204645172 létrehozása ...
Testrail-mysql-204645172 létrehozása ... kész
Testrail-migráció-204645172 létrehozása ... kész
Testrail-fpm-204645172 ... kész
Testrail-web-204645172 ... kész
docker ps
KONTÉNER AZONOSÍTÓ KÉP PARANCS LÉTREHOZVA ÁLLAPOT PORTOK NEVEK
c6b76f9135ed registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 13 másodperce Futtatás 1 másodperce 0.0.0.0:51148->80/tcp, 0.0.0.0:25426->443/tcp testrail-web-204645172
01d303262d8e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 16 másodperce Futtatás 13 másodperce 9000/tcp testrail-fpm-204645172
2cdab1edbf6a registry.gitlab.com/touchbit/image/testrail/migration:latest "docker-entrypoint.s…" 16 másodperce Futtatás 13 másodperce 3306/tcp, 33060/tcp testrail-migration-204645172
826aaf7c0a29 mysql:5.7.22 "docker-entrypoint.s…" 18 másodperce Futtatás 16 másodperce 3306/tcp testrail-mysql-204645172
6dbb3fae0322 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 36 másodperce Futtatás 22 másodperce 0.0.0.0:44202->80/tcp, 0.0.0.0:20151->443/tcp testrail-web-204645084
3540f8d448ce registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 38 másodperce Futtatás 35 másodperce 9000/tcp testrail-fpm-204645084
70fea72aa10d mysql:5.7.22 "docker-entrypoint.s…" 40 másodperce Futtatás 37 másodperce 3306/tcp testrail-mysql-204645084
d8aa24b2892d registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" Körülbelül egy perce Futtatás 53 másodperce 0.0.0.0:31103->80/tcp, 0.0.0.0:43872->443/tcp testrail-web-204644881
6d4ccd910fad registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" Körülbelül egy perce Futtatás Körülbelül egy perce 9000/tcp testrail-fpm-204644881
685d8023a3ec mysql:5.7.22 "docker-entrypoint.s…" Körülbelül egy perce Futtatás Körülbelül egy perce 3306/tcp testrail-mysql-204644881
1cdfc692003a registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" Körülbelül egy perce Futtatás Körülbelül egy perce 0.0.0.0:44752->80/tcp, 0.0.0.0:23540->443/tcp testrail-web-204644793
6f26dfb2683e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" Körülbelül egy perce Futtatás Körülbelül egy perce 9000/tcp testrail-fpm-204644793
029e16b26201 mysql:5.7.22 "docker-entrypoint.s…" Körülbelül egy perce Futtatás Körülbelül egy perce 3306/tcp testrail-mysql-204644793
c10443222ac6 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 órája Futtatás 5 órája 0.0.0.0:57123->80/tcp, 0.0.0.0:31657->443/tcp testrail-web-204567103
04339229397e registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 órája Futtatás 5 órája 9000/tcp testrail-fpm-204567103
6ae0accab28d mysql:5.7.22 "docker-entrypoint.s…" 5 órája Futtatás 5 órája 3306/tcp testrail-mysql-204567103
b66b60d79e43 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 órája Futtatás 5 órája 0.0.0.0:56321->80/tcp, 0.0.0.0:58749->443/tcp testrail-web-204553690
033b1f46afa9 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 órája Futtatás 5 órája 9000/tcp testrail-fpm-204553690
a8879c5ef941 mysql:5.7.22 "docker-entrypoint.s…" 5 órája Futtatás 5 órája 3306/tcp testrail-mysql-204553690
069954ba6010 registry.gitlab.com/touchbit/image/testrail/web:latest "nginx -g 'daemon of…" 5 órája Futtatás 5 órája 0.0.0.0:32869->80/tcp, 0.0.0.0:16066->443/tcp testrail-web-204553539
ed6b17d911a5 registry.gitlab.com/touchbit/image/testrail/fpm:latest "docker-php-entrypoi…" 5 órája Futtatás 5 órája 9000/tcp testrail-fpm-204553539
1a1eed057ea0 mysql:5.7.22 "docker-entrypoint.s…" 5 órája Futtatás 5 órája 3306/tcp testrail-mysql-204553539Minden feladat sikeresen befejeződött
A feladat artefaktumai a szolgáltatások és tesztek naplóit tartalmazzák
Úgy tűnik, minden rendben van, de van egy kis probléma. A Pipeline képes kényszerített leállításra az integrációs tesztek futása alatt, és ebben az esetben a futó konténerek nem fognak leállni. Időnként szükséges a futtató takarítása. Sajnos a GitLab CE-ben a feladatra vonatkozó javítás még mindig állapotban van.
De nálunk be lett állítva a feladat ütemezett indítása, és senki sem akadályozza meg, hogy manuálisan is elindítsuk.
Lépjünk be a projektünkbe -> CI/CD -> Ütemezések és indítsuk el a feladatot Futtató tisztítása
Összesen:
- Van egy shell futtatónk.
- Nincsenek konfliktusok a feladatok és a környezet között.
- Paralel futtatásaink vannak integrációs tesztekkel.
- Integrációs teszteket futtathatunk helyben is, valamint konténerben is.
- A szolgáltatások és a tesztek naplói összegyűjtésre kerülnek és csatolva lesznek a pipeline feladathoz.
- Lehetőség van a régi docker-képek eltávolítására a futtatóból.
A beállítási idő körülbelül 2 óra.
Itt van, gyakorlatilag ennyi. Örömmel várom a visszajelzést.
Forrás: habr.com
