From cb5045fb79167eac22115c20be18cc6af6e44d46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A1=D0=B5=D1=80=D0=B3=D0=B5=D0=B9=20=D0=90=D0=BD=D1=82?= =?UTF-8?q?=D1=80=D0=BE=D0=BF=D0=BE=D0=B2?= Date: Wed, 29 Oct 2025 18:53:52 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D1=80=D0=BE=D0=BB=D0=B8=20devops=20=D0=B8?= =?UTF-8?q?=20=D1=82=D0=B5=D1=81=D1=82=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Убрана подстановка значений по умолчанию для devops_password и devops_ssh_public_key - Добавлена строгая валидация секретов из vault/secrets.yml с детальными сообщениями об ошибках - Убран подробный вывод установки пакетов в тасках - Исправлена проблема с созданием симлинков в vault/ при тестировании - Обновлена логика загрузки vault переменных в molecule тестах - Добавлена очистка симлинков в destroy.yml для дополнительной безопасности Автор: Сергей Антропов Сайт: https://devops.org.ru --- Makefile | 111 ++------- README.md | 4 +- dockerfiles/README.md | 2 +- docs/cicd-setup.md | 8 +- docs/creating-roles.md | 2 +- docs/dockerfiles.md | 2 +- docs/molecule-guide.md | 8 +- docs/monitoring.md | 12 +- docs/run-yml-guide.md | 248 +++++++++++++++++++ docs/site-yml-guide.md | 8 +- docs/universal-testing.md | 2 +- molecule/default/converge.yml | 329 ++++++++++++++++++------- molecule/default/create.yml | 77 +----- molecule/default/destroy.yml | 74 ++++++ molecule/default/{site.yml => run.yml} | 151 ++++++------ roles/deploy.yml | 16 +- roles/devops/README.md | 8 +- roles/devops/defaults/main.yml | 8 +- roles/devops/tasks/main.yml | 76 ++++-- roles/devops/vars/main.yml | 1 + scripts/test-custom-images.sh | 243 ------------------ scripts/update-playbooks.sh | 10 +- vault/secrets.yml | 100 ++++---- 23 files changed, 821 insertions(+), 679 deletions(-) create mode 100644 docs/run-yml-guide.md rename molecule/default/{site.yml => run.yml} (68%) delete mode 100755 scripts/test-custom-images.sh diff --git a/Makefile b/Makefile index 2d18ad8..fc90adb 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,27 @@ role: -e MOLECULE_EPHEMERAL_DIRECTORY=/tmp/molecule_workspace \ -e MOLECULE_VAULT_ENABLED=$${MOLECULE_VAULT_ENABLED:-false} \ $(DOCKER_IMAGE) \ - bash -c "mkdir -p /tmp/molecule_workspace/inventory && cd molecule/default && ansible-playbook -i localhost, create.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace && ansible-playbook -i localhost, converge.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace && ansible-playbook -i /tmp/molecule_workspace/inventory/hosts.ini site.yml; ansible-playbook -i localhost, destroy.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace; echo '✅ Тестирование завершено'";; \ + bash -c " \ + echo '=== СОЗДАНИЕ ТЕСТОВЫХ КОНТЕЙНЕРОВ ==='; \ + mkdir -p /tmp/molecule_workspace/inventory && \ + cd molecule/default && \ + ansible-playbook -i localhost, create.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace && \ + echo ''; \ + echo '=== НАСТРОЙКА VAULT И ПЕРЕМЕННЫХ ==='; \ + ansible-playbook -i localhost, converge.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace && \ + echo ''; \ + echo '=== ПРОВЕРКА ПОДКЛЮЧЕНИЯ К КОНТЕЙНЕРАМ ==='; \ + ansible all -i /tmp/molecule_workspace/inventory/hosts.ini -m ping && \ + echo ''; \ + echo '=== ЗАПУСК RUN.YML НА ТЕСТОВЫХ КОНТЕЙНЕРАХ ==='; \ + ansible-playbook -i /tmp/molecule_workspace/inventory/hosts.ini run.yml -v -e vault_devops_password="123123" -e vault_devops_ssh_public_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBjzRt/b5Xe/tgQS2rvOBXOSBq1hychcnbz6G4m9Ps6hQXCxLA0hcrzPIRGazeWEslqsBynSm4fVJC6zAnExEd7KsNxS5gsMxmcHsghuU6/IA62tP8w8tXKEWaCGMQyfcUO/MIrdEjAg8txl3FIxdlcYwBTLW9nJggOmUn9w1YOA6ECNBDUbTwZC62yomJhQoAK0W+uVkKSLTqRIvd0oZJEF+0dtzBrhhe7cjR6fuoLpkB1/Q9bQImVfAxEiiExhFWFMxcyf4SGxpmsbKI4rJ3eBvsMmhrX76p1bYX4fKGiBaqNyXqThYWYybXfDfaITQR87SIrVt4U4NzS79ZFfQ142VPs+YISiy/+/VKZ1NjHo1fRZJSqBtsWJCsvtuM6C2+dRZ0JqwwMlHKLNhmerYMLJMQxkxdB5jRxafC+3T0aFNIsFIa7MdC8i3WQBk5z5huY5pslkPWnmKTfCi3gLjWNhfW9xEgKAww6hGrZR/zlQXZQrmQ2LGspzXFngd9tmk= linux@key" && \ + echo ''; \ + echo '=== ЗАПУСК ROLES/DEPLOY.YML НА ТЕСТОВЫХ КОНТЕЙНЕРАХ ==='; \ + ansible-playbook -i /tmp/molecule_workspace/inventory/hosts.ini ../../roles/deploy.yml -v -e vault_devops_password="123123" -e vault_devops_ssh_public_key="ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDBjzRt/b5Xe/tgQS2rvOBXOSBq1hychcnbz6G4m9Ps6hQXCxLA0hcrzPIRGazeWEslqsBynSm4fVJC6zAnExEd7KsNxS5gsMxmcHsghuU6/IA62tP8w8tXKEWaCGMQyfcUO/MIrdEjAg8txl3FIxdlcYwBTLW9nJggOmUn9w1YOA6ECNBDUbTwZC62yomJhQoAK0W+uVkKSLTqRIvd0oZJEF+0dtzBrhhe7cjR6fuoLpkB1/Q9bQImVfAxEiiExhFWFMxcyf4SGxpmsbKI4rJ3eBvsMmhrX76p1bYX4fKGiBaqNyXqThYWYybXfDfaITQR87SIrVt4U4NzS79ZFfQ142VPs+YISiy/+/VKZ1NjHo1fRZJSqBtsWJCsvtuM6C2+dRZ0JqwwMlHKLNhmerYMLJMQxkxdB5jRxafC+3T0aFNIsFIa7MdC8i3WQBk5z5huY5pslkPWnmKTfCi3gLjWNhfW9xEgKAww6hGrZR/zlQXZQrmQ2LGspzXFngd9tmk= linux@key" && \ + echo ''; \ + echo '=== ОЧИСТКА РЕСУРСОВ ==='; \ + ansible-playbook -i localhost, destroy.yml --connection=local -e molecule_ephemeral_directory=/tmp/molecule_workspace && \ + echo '✅ Тестирование завершено'";; \ deploy) \ echo "🚀 Развертывание ролей на реальные серверы..."; \ echo ""; \ @@ -223,26 +243,6 @@ presets: echo ""; \ echo "🐳 Образы:"; \ grep -E "^- " "molecule/presets/$(PRESET).yml" | grep -E "family:" | sed 's/.*family: / - /' || echo "Образы не найдены";; \ - test) \ - if [ -z "$(PRESET)" ]; then \ - echo "❌ Ошибка: Укажите PRESET=имя_пресета"; \ - echo "💡 Пример: make presets test PRESET=etcd-patroni"; \ - exit 1; \ - fi; \ - if [ ! -f "molecule/presets/$(PRESET).yml" ]; then \ - echo "❌ Ошибка: Пресет '$(PRESET)' не найден!"; \ - echo "💡 Доступные пресеты:"; \ - make presets list; \ - exit 1; \ - fi; \ - echo "🚀 Тестирование с пресетом: $(PRESET)"; \ - echo ""; \ - docker run --rm --name $(CONTAINER_NAME) -v "$(PWD):/workspace" -w /workspace \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e ANSIBLE_FORCE_COLOR=1 \ - -e MOLECULE_PRESET=$(PRESET) \ - $(DOCKER_IMAGE) \ - bash -c "cd molecule/default && molecule test" || echo "✅ Тестирование завершено";; \ *) \ echo "🎯 Доступные команды:"; \ echo ""; \ @@ -253,15 +253,9 @@ presets: echo " 💡 Показывает: описание, хосты, сеть, образы"; \ echo " 💡 Требует: PRESET=имя_пресета"; \ echo ""; \ - echo " 🚀 make presets test - запустить тест с preset'ом"; \ - echo " 💡 Запускает: molecule test с выбранным preset'ом"; \ - echo " 💡 Требует: PRESET=имя_пресета"; \ - echo ""; \ echo "💡 Примеры:"; \ echo " make presets list # показать все preset'ы"; \ - echo " make presets info PRESET=etcd-patroni # информация о etcd-patroni"; \ - echo " make presets test PRESET=minimal # тест с minimal preset"; \ - echo " make presets test PRESET=performance # тест с performance preset";; \ + echo " make presets info PRESET=etcd-patroni # информация о etcd-patroni";; \ esac # ============================================================================= @@ -1571,12 +1565,6 @@ help: @echo "📋 PRESET'Ы (тестовые окружения):" @echo " make presets list - показать все доступные preset'ы" @echo " make presets info - подробная информация о preset'е" - @echo " make presets test - запустить тест с preset'ом" - @echo "" - @echo "🖼️ СОБСТВЕННЫЕ ОБРАЗЫ (DevOpsLab):" - @echo " make custom-images test [minimal|full|performance] - тест с собственными образами" - @echo " make custom-images check - проверить наличие собственных образов" - @echo " make custom-images build - собрать все образы для тестирования" @echo "" @echo "🐳 DOCKER ОБРАЗЫ (Multi-Arch):" @echo " make docker prepare - подготовка к работе с Docker Hub" @@ -1641,7 +1629,6 @@ help: @echo "" @echo "💡 ПРИМЕРЫ ИСПОЛЬЗОВАНИЯ:" @echo " make presets list # показать все preset'ы" - @echo " make presets test PRESET=etcd-patroni # тест с etcd-patroni" @echo " make role test minimal # быстрый тест" @echo " make role test all-images # тест всех образов" @echo " make docker setup-builder # настройка multi-arch builder" @@ -1658,60 +1645,6 @@ help: @echo "📖 Подробная справка: make [команда]" @echo "==========================================" -# ============================================================================= -# КОМАНДЫ ДЛЯ РАБОТЫ С СОБСТВЕННЫМИ ОБРАЗАМИ -# ============================================================================= -custom-images: - @case "$(word 2, $(MAKECMDGOALS))" in \ - test) \ - echo "🧪 Тестирование с собственными образами DevOpsLab..."; \ - if [ -z "$(word 3, $(MAKECMDGOALS))" ]; then \ - echo "💡 Использование: make custom-images test [minimal|full|performance]"; \ - echo "💡 По умолчанию: minimal"; \ - ./scripts/test-custom-images.sh minimal; \ - else \ - ./scripts/test-custom-images.sh $(word 3, $(MAKECMDGOALS)); \ - fi;; \ - check) \ - echo "🔍 Проверка наличия собственных образов..."; \ - ./scripts/test-custom-images.sh check;; \ - build) \ - echo "🔨 Сборка всех образов для тестирования..."; \ - $(MAKE) docker build;; \ - *) \ - echo "🎯 Доступные команды:"; \ - echo ""; \ - echo " 🧪 make custom-images test [minimal|full|performance] - тест с собственными образами"; \ - echo " 💡 minimal - минимальный тест (4 хоста)"; \ - echo " 💡 full - полный тест (все образы)"; \ - echo " 💡 performance - тест производительности (8 хостов)"; \ - echo ""; \ - echo " 🔍 make custom-images check - проверить наличие собственных образов"; \ - echo " 💡 Показывает: какие образы есть, какие отсутствуют"; \ - echo " 💡 Предлагает: команды для сборки отсутствующих образов"; \ - echo ""; \ - echo " 🔨 make custom-images build - собрать все образы для тестирования"; \ - echo " 💡 Выполняет: make docker build"; \ - echo " 💡 Собирает: все образы DevOpsLab"; \ - echo ""; \ - echo "💡 Пресеты для тестирования:"; \ - echo " - custom-minimal.yml - минимальный тест (4 хоста)"; \ - echo " - custom-images.yml - полный тест (все образы)"; \ - echo " - custom-performance.yml - тест производительности (8 хостов)"; \ - echo ""; \ - echo "💡 Собственные образы:"; \ - echo " - inecs/ansible-lab:ansible-controller-latest"; \ - echo " - inecs/ansible-lab:alt9-latest"; \ - echo " - inecs/ansible-lab:alt10-latest"; \ - echo " - inecs/ansible-lab:astra-linux-latest"; \ - echo " - inecs/ansible-lab:redos-latest"; \ - echo " - inecs/ansible-lab:rhel-latest"; \ - echo " - inecs/ansible-lab:centos-latest"; \ - echo " - inecs/ansible-lab:alma-latest"; \ - echo " - inecs/ansible-lab:rocky-latest"; \ - echo " - inecs/ansible-lab:ubuntu-latest"; \ - echo " - inecs/ansible-lab:debian-latest";; \ - esac # ============================================================================= # АВТОМАТИЗАЦИЯ diff --git a/README.md b/README.md index aaf3f2b..2d6e731 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ DevOpsLab/ │ │ ├── create.yml # Создание контейнеров │ │ ├── converge.yml # Запуск тестов │ │ ├── destroy.yml # Удаление контейнеров -│ │ ├── site.yml # Основной playbook +│ │ ├── run.yml # Основной playbook для обновления контейнеров │ │ ├── verify.yml # Проверка конфигурации │ │ └── molecule.yml # Конфигурация Molecule │ └── presets/ # Preset конфигурации @@ -596,7 +596,7 @@ make custom-images # справка по собственным ### Развертывание и конфигурация -- **[docs/site-yml-guide.md](docs/site-yml-guide.md)** - Руководство по файлу site.yml +- **[docs/run-yml-guide.md](docs/run-yml-guide.md)** - Руководство по файлу run.yml - **[docs/deploy-yml-customization.md](docs/deploy-yml-customization.md)** - Полное руководство по кастомизации deploy.yml ### Безопасность и качество diff --git a/dockerfiles/README.md b/dockerfiles/README.md index c34fcc1..6d24a5e 100644 --- a/dockerfiles/README.md +++ b/dockerfiles/README.md @@ -111,7 +111,7 @@ docker run --rm \ -v $(pwd):/workspace \ -w /workspace \ inecs/ansible-lab:ansible-controller-latest \ - ansible-playbook site.yml + ansible-playbook run.yml ``` ### 2. k8s diff --git a/docs/cicd-setup.md b/docs/cicd-setup.md index 9274506..00b16a0 100644 --- a/docs/cicd-setup.md +++ b/docs/cicd-setup.md @@ -91,7 +91,7 @@ jobs: run: | git config --local user.email "action@github.com" git config --local user.name "GitHub Action" - git add molecule/default/site.yml roles/deploy.yml + git add molecule/default/run.yml roles/deploy.yml git diff --staged --quiet || git commit -m "Auto-update playbooks for new roles" git push @@ -272,7 +272,7 @@ stages: - script: | git config --local user.email "action@azure.com" git config --local user.name "Azure DevOps" - git add molecule/default/site.yml deploy.yml + git add molecule/default/run.yml deploy.yml git diff --staged --quiet || git commit -m "Auto-update playbooks for new roles" git push displayName: 'Commit Changes' @@ -480,7 +480,7 @@ pipeline { sh ''' git config --local user.email "jenkins@example.com" git config --local user.name "Jenkins" - git add molecule/default/site.yml roles/deploy.yml + git add molecule/default/run.yml roles/deploy.yml git diff --staged --quiet || git commit -m "Auto-update playbooks for new roles" git push ''' @@ -656,7 +656,7 @@ update-playbooks: - ./scripts/update-playbooks.sh - git config --local user.email "gitlab@example.com" - git config --local user.name "GitLab CI" - - git add molecule/default/site.yml deploy.yml + - git add molecule/default/run.yml deploy.yml - git diff --staged --quiet || git commit -m "Auto-update playbooks for new roles" - git push only: diff --git a/docs/creating-roles.md b/docs/creating-roles.md index 8475d8d..1c2cd48 100644 --- a/docs/creating-roles.md +++ b/docs/creating-roles.md @@ -415,7 +415,7 @@ make role deploy ### 1. Автоматическое включение в playbook'и Роль автоматически включается в: -- `molecule/default/site.yml` (для тестирования) +- `molecule/default/run.yml` (для тестирования) - `roles/deploy.yml` (для продакшн развертывания) ### 2. Обновление playbook'ов diff --git a/docs/dockerfiles.md b/docs/dockerfiles.md index 6f36a4e..c45804b 100644 --- a/docs/dockerfiles.md +++ b/docs/dockerfiles.md @@ -88,7 +88,7 @@ docker run --rm \ -v $(pwd):/workspace \ -w /workspace \ inecs/ansible-lab:ansible-controller-latest \ - ansible-playbook site.yml + ansible-playbook run.yml ``` ### k8s diff --git a/docs/molecule-guide.md b/docs/molecule-guide.md index 03ae6b8..05f20fb 100644 --- a/docs/molecule-guide.md +++ b/docs/molecule-guide.md @@ -26,7 +26,7 @@ molecule/ │ ├── converge.yml # Выполнение ролей в контейнерах │ ├── verify.yml # Проверка результатов тестирования │ ├── destroy.yml # Удаление тестовых контейнеров -│ └── site.yml # Основной playbook для тестирования +│ └── run.yml # Основной playbook для обновления контейнеров └── presets/ # Preset конфигурации для разных сценариев ├── minimal.yml # Минимальный preset (1 хост) ├── performance.yml # Performance preset (12 хостов) @@ -392,12 +392,12 @@ vars: command: > bash -lc " ANSIBLE_ROLES_PATH=/workspace/roles - ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.ini /workspace/molecule/default/site.yml + ansible-playbook -i {{ lookup('env','MOLECULE_EPHEMERAL_DIRECTORY') }}/inventory/hosts.ini /workspace/molecule/default/run.yml " ``` - **Назначение:** Выполнение основного playbook'а - **Функции:** - - Запуск `site.yml` в ansible-controller контейнере + - Запуск `run.yml` в ansible-controller контейнере - Использование сгенерированного инвентори - Установка пути к ролям @@ -602,7 +602,7 @@ vars: - **Назначение:** Отображение сводки по очистке - **Функция:** Информация о удаленных ресурсах -### 6. `molecule/default/site.yml` - Основной playbook +### 6. `molecule/default/run.yml` - Основной playbook **Назначение:** Основной playbook для тестирования ролей. diff --git a/docs/monitoring.md b/docs/monitoring.md index 2899e52..2286197 100644 --- a/docs/monitoring.md +++ b/docs/monitoring.md @@ -290,13 +290,13 @@ make clean-containers make role lint # Тест с verbose выводом -ansible-playbook -i inventory/hosts.ini site.yml -vvv +ansible-playbook -i inventory/hosts.ini run.yml -vvv # Проверка переменных ansible-inventory --list -i inventory/hosts.ini # Dry-run без выполнения -ansible-playbook -i inventory/hosts.ini site.yml --check +ansible-playbook -i inventory/hosts.ini run.yml --check ``` ### Проблемы с ролями @@ -318,14 +318,14 @@ yamllint roles/docker/tasks/main.yml ```bash # Проверка переменных -ansible-playbook -i inventory/hosts.ini site.yml --list-tags -ansible-playbook -i inventory/hosts.ini site.yml --list-tasks +ansible-playbook -i inventory/hosts.ini run.yml --list-tags +ansible-playbook -i inventory/hosts.ini run.yml --list-tasks # Выполнение конкретной задачи -ansible-playbook -i inventory/hosts.ini site.yml --tags docker +ansible-playbook -i inventory/hosts.ini run.yml --tags docker # Debug режим -ansible-playbook -i inventory/hosts.ini site.yml -vvv +ansible-playbook -i inventory/hosts.ini run.yml -vvv ``` ## 📈 Сбор диагностической информации diff --git a/docs/run-yml-guide.md b/docs/run-yml-guide.md new file mode 100644 index 0000000..526c6ae --- /dev/null +++ b/docs/run-yml-guide.md @@ -0,0 +1,248 @@ +# Руководство по файлу run.yml + +**Автор:** Сергей Антропов +**Сайт:** https://devops.org.ru + +## 📋 Описание + +Файл `molecule/default/run.yml` является **универсальным playbook'ом для обновления тестовых контейнеров**. Этот файл отвечает за подготовку окружения и установку необходимых утилит внутри контейнеров при запуске тестов. + +## 🎯 Назначение + +### Основные функции: + +1. **Обновление пакетов** в контейнерах при запуске тестов +2. **Установка common tools** для корректной работы тестов +3. **Подготовка окружения** для тестирования ролей +4. **Импорт roles/deploy.yml** для запуска ролей + +## 🏗️ Структура файла + +### 1. Подготовка окружения для тестирования + +```yaml +- name: Подготовка окружения для тестирования + hosts: all + become: true + tasks: + # Задачи подготовки... +``` + +**Что делает:** +- Обновляет кеш пакетов для всех поддерживаемых ОС +- Устанавливает необходимые утилиты +- Настраивает пользователя для тестирования +- Создает рабочие директории + +### 2. Импорт deploy.yml + +```yaml +- import_playbook: ../../roles/deploy.yml +``` + +**Что делает:** +- Импортирует `roles/deploy.yml` для запуска ролей +- Разделяет логику: `run.yml` - подготовка, `deploy.yml` - роли +- Обеспечивает единую точку управления ролями + +## 🐧 Поддерживаемые ОС + +### Debian/Ubuntu +- **Менеджер пакетов:** `apt` +- **Обновление:** `apt update` +- **Утилиты:** `curl`, `jq`, `ca-certificates`, `iproute2`, `iputils-ping`, `procps`, `net-tools`, `sudo`, `vim`, `wget`, `unzip`, `git` + +### RHEL/CentOS/AlmaLinux/Rocky +- **Менеджер пакетов:** `yum` +- **Обновление:** `yum update_cache` +- **Утилиты:** `curl`, `jq`, `ca-certificates`, `iproute`, `iputils`, `procps-ng`, `net-tools`, `sudo`, `vim`, `wget`, `unzip`, `git` + +### Alt Linux +- **Менеджер пакетов:** `apt` (специальная версия) +- **Обновление:** `apt update` +- **Утилиты:** `curl`, `jq`, `ca-certificates`, `iproute2`, `iputils`, `procps`, `net-tools`, `sudo`, `vim`, `wget`, `unzip`, `git` + +## 🏷️ Теги (Tags) + +### setup +- Обновление пакетов +- Установка утилит +- Настройка пользователей +- Создание директорий + +### update +- Обновление кеша пакетов +- Обновление списка пакетов + +### tools +- Установка common tools +- Установка системных утилит + +### python +- Установка Python 3 +- Установка pip +- Установка venv + +### user +- Создание тестового пользователя +- Настройка домашней директории + +### sudo +- Настройка sudo для тестового пользователя +- Конфигурация прав доступа + +### directory +- Создание рабочих директорий +- Настройка прав доступа + +### roles +- Запуск тестирования ролей +- Выполнение функциональных тестов + +### test +- Тестирование функциональности +- Проверка работоспособности + +## 🚀 Использование + +### Запуск полного тестирования +```bash +make role test +``` + +### Запуск только подготовки окружения +```bash +make role test --tags setup +``` + +### Запуск только обновления пакетов +```bash +make role test --tags update +``` + +### Запуск только установки утилит +```bash +make role test --tags tools +``` + +### Запуск только тестирования ролей +```bash +make role test --tags roles +``` + +## 🔧 Настройка + +### Переменные окружения + +```bash +# Настройка тестового пользователя +export TEST_USER=testuser + +# Настройка рабочей директории +export TEST_DIR=/tmp/ansible-test + +# Настройка прав доступа +export TEST_MODE=0755 +``` + +### Кастомизация утилит + +Для добавления дополнительных утилит отредактируйте соответствующие секции: + +```yaml +# Для Debian/Ubuntu +- name: Install common tools (Debian/Ubuntu) + apt: + name: + - curl + - jq + - your-custom-tool # Добавьте сюда + state: present +``` + +## 🐛 Troubleshooting + +### Проблема: Ошибка обновления пакетов +**Решение:** Проверьте доступность репозиториев и интернет-соединение + +### Проблема: Не удается установить утилиты +**Решение:** Проверьте названия пакетов для конкретной ОС + +### Проблема: Ошибка создания пользователя +**Решение:** Проверьте права доступа и существование пользователя + +### Проблема: Ошибка настройки sudo +**Решение:** Проверьте синтаксис файла sudoers + +## 📊 Мониторинг + +### Логи выполнения +```bash +# Просмотр логов тестирования +make role test 2>&1 | tee test.log + +# Фильтрация по тегам +grep "TASK \[.*\]" test.log +``` + +### Проверка установленных утилит +```bash +# В контейнере +which curl jq vim git +``` + +### Проверка пользователя +```bash +# В контейнере +id testuser +sudo -l -U testuser +``` + +## 🔄 Автоматическое обновление + +Файл `run.yml` автоматически обновляется при добавлении новых ролей: + +```bash +# Автоматическое обновление +make update-playbooks +``` + +**Что происходит:** +1. Обнаруживаются все роли в директории `roles/` +2. Обновляется секция "Тестирование всех ролей" +3. Добавляются новые роли в список + +## 📝 Примеры использования + +### Тестирование конкретной роли +```bash +# Тестирование только роли ping +make role test minimal ping +``` + +### Тестирование с конкретным preset'ом +```bash +# Тестирование с preset'ом performance +make role test performance +``` + +### Отладка проблем +```bash +# Запуск с подробным выводом +make role test --verbose +``` + +## 🎯 Лучшие практики + +1. **Всегда используйте теги** для разделения задач +2. **Проверяйте совместимость** утилит с ОС +3. **Тестируйте на разных образах** перед коммитом +4. **Используйте idempotent задачи** для стабильности +5. **Документируйте изменения** в комментариях + +## 🔗 Связанные файлы + +- `molecule/default/molecule.yml` - конфигурация Molecule +- `roles/deploy.yml` - playbook для продакшн развертывания +- `inventory/hosts.ini` - инвентори для тестирования +- `Makefile` - команды для запуска тестов diff --git a/docs/site-yml-guide.md b/docs/site-yml-guide.md index e95f1ae..526c6ae 100644 --- a/docs/site-yml-guide.md +++ b/docs/site-yml-guide.md @@ -1,11 +1,11 @@ -# Руководство по файлу site.yml +# Руководство по файлу run.yml **Автор:** Сергей Антропов **Сайт:** https://devops.org.ru ## 📋 Описание -Файл `molecule/default/site.yml` является **универсальным playbook'ом для тестирования Ansible ролей** в контейнерах. Этот файл отвечает за подготовку окружения и установку необходимых утилит внутри контейнеров при запуске тестов. +Файл `molecule/default/run.yml` является **универсальным playbook'ом для обновления тестовых контейнеров**. Этот файл отвечает за подготовку окружения и установку необходимых утилит внутри контейнеров при запуске тестов. ## 🎯 Назначение @@ -42,7 +42,7 @@ **Что делает:** - Импортирует `roles/deploy.yml` для запуска ролей -- Разделяет логику: `site.yml` - подготовка, `deploy.yml` - роли +- Разделяет логику: `run.yml` - подготовка, `deploy.yml` - роли - Обеспечивает единую точку управления ролями ## 🐧 Поддерживаемые ОС @@ -200,7 +200,7 @@ sudo -l -U testuser ## 🔄 Автоматическое обновление -Файл `site.yml` автоматически обновляется при добавлении новых ролей: +Файл `run.yml` автоматически обновляется при добавлении новых ролей: ```bash # Автоматическое обновление diff --git a/docs/universal-testing.md b/docs/universal-testing.md index 6aa02cd..5bc0c84 100644 --- a/docs/universal-testing.md +++ b/docs/universal-testing.md @@ -136,7 +136,7 @@ make vault decrypt ├── files/ │ ├── requirements.yml │ └── playbooks/ -│ └── site.yml +│ └── run.yml ├── vault/ │ └── secrets.yml └── Makefile diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml index 22694bb..a8ba329 100644 --- a/molecule/default/converge.yml +++ b/molecule/default/converge.yml @@ -13,9 +13,11 @@ vault_targets: - /workspace/vault/secrets.yml - /workspace/vault/secret.yml - - /workspace/files/playbooks/group_vars/*/vault.yml - - /workspace/files/playbooks/host_vars/*/vault.yml - - /workspace/roles/**/vars/vault.yml +# - /workspace/files/playbooks/group_vars/*/vault.yml +# - /workspace/files/playbooks/host_vars/*/vault.yml +# - /workspace/roles/**/vars/vault.yml +# - /workspace/roles/*/defaults/*.yml +# - /workspace/files/**/*secret*.yml tasks: # ============================================================================= @@ -35,10 +37,6 @@ when: preset_file is file ignore_errors: true -# - name: Install collections -# community.docker.docker_container_exec: -# container: ansible-controller -# command: bash -lc "ansible-galaxy collection install -r /workspace/requirements.yml --force --no-deps --upgrade >/dev/null 2>&1 || true" # ============================================================================= # VAULT - Работа с зашифрованными файлами @@ -52,99 +50,250 @@ Files: {{ vault_targets | length }} targets ================================================================================ - - name: Check if vault file is encrypted - community.docker.docker_container_exec: - container: ansible-controller - command: "bash -c 'if [ -f \"/workspace/vault/secrets.yml\" ]; then grep -q \"ANSIBLE_VAULT\" /workspace/vault/secrets.yml && echo \"ENCRYPTED\" || echo \"PLAINTEXT\"; else echo \"NOT_FOUND\"; fi'" - register: vault_status - ignore_errors: true - - - name: Encrypt vault file if plaintext - community.docker.docker_container_exec: - container: ansible-controller - command: "bash -c 'VAULT_PASSWORD_FILE=\"/workspace/vault/.vault\"; if [ -f \"$VAULT_PASSWORD_FILE\" ] && [ -f \"/workspace/vault/secrets.yml\" ] && [ \"{{ vault_status.stdout }}\" = \"PLAINTEXT\" ]; then ansible-vault encrypt --encrypt-vault-id default --vault-password-file \"$VAULT_PASSWORD_FILE\" /workspace/vault/secrets.yml; fi'" - when: vault_status.stdout == "PLAINTEXT" - ignore_errors: true - - - name: Preflight vault — normalize state (encrypt if plaintext, then decrypt) - community.docker.docker_container_exec: - container: ansible-controller - command: "bash -c 'VAULT_PASSWORD_FILE=\"/workspace/vault/.vault\"; if [ -f \"$VAULT_PASSWORD_FILE\" ] && [ -f \"/workspace/vault/secrets.yml\" ]; then ansible-vault decrypt --vault-password-file \"$VAULT_PASSWORD_FILE\" /workspace/vault/secrets.yml; fi'" - ignore_errors: true - - # ============================================================================= - # PLAYBOOK - Запуск основного playbook - # ============================================================================= - - name: Playbook execution - debug: - msg: | - ================================================================================ - PLAYBOOK - Запуск основного playbook - ================================================================================ - File: /workspace/molecule/default/site.yml - ================================================================================ - - - name: Debug - Check files in container + - name: Check vault files encryption status community.docker.docker_container_exec: container: ansible-controller command: | bash -c ' - echo "=== DEBUG INFO ===" - echo "Current directory: $(pwd)" - echo "ANSIBLE_ROLES_PATH: $ANSIBLE_ROLES_PATH" - echo "VAULT_PASSWORD_FILE: $VAULT_PASSWORD_FILE" - echo "VAULT_SECRETS_FILE: $VAULT_SECRETS_FILE" - echo "INVENTORY_FILE: $INVENTORY_FILE" - echo "" - echo "=== FILE CHECKS ===" - echo "Inventory exists: $([ -f "/tmp/molecule_workspace/inventory/hosts.ini" ] && echo "YES" || echo "NO")" - echo "Vault password exists: $([ -f "/workspace/vault/.vault" ] && echo "YES" || echo "NO")" - echo "Vault secrets exists: $([ -f "/workspace/vault/secrets.yml" ] && echo "YES" || echo "NO")" - echo "Site.yml exists: $([ -f "/workspace/molecule/default/site.yml" ] && echo "YES" || echo "NO")" - echo "" - echo "=== DIRECTORY LISTING ===" - ls -la /tmp/molecule_workspace/ || echo "No molecule_workspace dir" - ls -la /workspace/vault/ || echo "No vault dir" - echo "" - echo "=== INVENTORY CONTENT ===" - cat /tmp/molecule_workspace/inventory/hosts.ini || echo "Cannot read inventory" + VAULT_TARGETS_JSON="{{ vault_targets | to_json }}" + VAULT_PASSWORD_FILE="/workspace/vault/.vault" + + echo "=== CHECKING VAULT FILES ENCRYPTION STATUS ===" + + # Парсим JSON массив и проверяем каждый файл + echo "$VAULT_TARGETS_JSON" | jq -r ".[]" | while read -r target; do + echo "Checking target: $target" + + # Если это glob паттерн, находим файлы + if [[ "$target" == *"*"* ]]; then + for file in $target; do + if [ -f "$file" ]; then + echo "Found file: $file" + if grep -q "ANSIBLE_VAULT" "$file"; then + echo "ENCRYPTED: $file" + else + echo "PLAINTEXT: $file" + fi + fi + done + else + # Обычный файл + if [ -f "$target" ]; then + echo "Found file: $target" + if grep -q "ANSIBLE_VAULT" "$target"; then + echo "ENCRYPTED: $target" + else + echo "PLAINTEXT: $target" + fi + else + echo "NOT_FOUND: $target" + fi + fi + done ' + register: vault_status_check + ignore_errors: true -# - name: Run lab playbook -# community.docker.docker_container_exec: -# container: ansible-controller -# command: | -# bash -c ' -# set -e -# export ANSIBLE_ROLES_PATH=/workspace/roles -# export VAULT_PASSWORD_FILE="/workspace/vault/.vault" -# export VAULT_SECRETS_FILE="/workspace/vault/secrets.yml" -# export INVENTORY_FILE="/tmp/molecule_workspace/inventory/hosts.ini" -# echo "Starting playbook execution..." -# if [ -f "$VAULT_PASSWORD_FILE" ] && [ -f "$VAULT_SECRETS_FILE" ]; then -# echo "Running with vault..." -# ansible-playbook -i "$INVENTORY_FILE" /workspace/molecule/default/site.yml --vault-password-file "$VAULT_PASSWORD_FILE" -e "vault_file_path=$VAULT_SECRETS_FILE" -v -# else -# echo "Running without vault..." -# ansible-playbook -i "$INVENTORY_FILE" /workspace/molecule/default/site.yml -v -# fi -# echo "Playbook completed successfully" -# ' + - name: Encrypt plaintext vault files + community.docker.docker_container_exec: + container: ansible-controller + command: | + bash -c ' + VAULT_TARGETS_JSON="{{ vault_targets | to_json }}" + VAULT_PASSWORD_FILE="/workspace/vault/.vault" + + echo "=== ENCRYPTING PLAINTEXT VAULT FILES ===" + + if [ ! -f "$VAULT_PASSWORD_FILE" ]; then + echo "Vault password file not found: $VAULT_PASSWORD_FILE" + exit 0 + fi + + # Парсим JSON массив и шифруем каждый plaintext файл + echo "$VAULT_TARGETS_JSON" | jq -r ".[]" | while read -r target; do + echo "Processing target: $target" + + # Если это glob паттерн, находим файлы + if [[ "$target" == *"*"* ]]; then + for file in $target; do + if [ -f "$file" ] && ! grep -q "ANSIBLE_VAULT" "$file"; then + echo "Encrypting plaintext file: $file" + ansible-vault encrypt --encrypt-vault-id default --vault-password-file "$VAULT_PASSWORD_FILE" "$file" + fi + done + else + # Обычный файл + if [ -f "$target" ] && ! grep -q "ANSIBLE_VAULT" "$target"; then + echo "Encrypting plaintext file: $target" + ansible-vault encrypt --encrypt-vault-id default --vault-password-file "$VAULT_PASSWORD_FILE" "$target" + fi + fi + done + ' + ignore_errors: true + + - name: Decrypt vault files for processing + community.docker.docker_container_exec: + container: ansible-controller + command: | + bash -c ' + VAULT_TARGETS_JSON="{{ vault_targets | to_json }}" + VAULT_PASSWORD_FILE="/workspace/vault/.vault" + + echo "=== DECRYPTING VAULT FILES FOR PROCESSING ===" + + if [ ! -f "$VAULT_PASSWORD_FILE" ]; then + echo "Vault password file not found: $VAULT_PASSWORD_FILE" + exit 0 + fi + + # Парсим JSON массив и расшифровываем каждый зашифрованный файл + echo "$VAULT_TARGETS_JSON" | jq -r ".[]" | while read -r target; do + echo "Processing target: $target" + + # Если это glob паттерн, находим файлы + if [[ "$target" == *"*"* ]]; then + for file in $target; do + if [ -f "$file" ] && grep -q "ANSIBLE_VAULT" "$file"; then + echo "Decrypting encrypted file: $file" + ansible-vault decrypt --vault-password-file "$VAULT_PASSWORD_FILE" "$file" + fi + done + else + # Обычный файл + if [ -f "$target" ] && grep -q "ANSIBLE_VAULT" "$target"; then + echo "Decrypting encrypted file: $target" + ansible-vault decrypt --vault-password-file "$VAULT_PASSWORD_FILE" "$target" + fi + fi + done + ' + ignore_errors: true # ============================================================================= - # CLEANUP - Перешифровка файлов после выполнения + # VAULT LOADING - Загрузка vault переменных из vault_targets # ============================================================================= - - name: Cleanup operations + - name: Load vault variables from vault_targets + community.docker.docker_container_exec: + container: ansible-controller + command: | + bash -c ' + VAULT_PASSWORD_FILE="/workspace/vault/.vault" + + # Читаем vault_targets из переменных Ansible + VAULT_TARGETS_JSON="{{ vault_targets | to_json }}" + + echo "=== VAULT LOADING ===" + echo "Vault password file: $VAULT_PASSWORD_FILE" + echo "Vault targets from Ansible: $VAULT_TARGETS_JSON" + + # Создаем директории для vault файлов + mkdir -p /tmp/vault_files + + # Создаем временный файл для объединения всех vault переменных + echo "---" > /tmp/vault_vars.yml + + # Счетчик для обработки конфликтов + declare -A variable_sources + + # Парсим JSON массив и обрабатываем каждый target + echo "$VAULT_TARGETS_JSON" | jq -r ".[]" | while read -r target; do + echo "Processing target: $target" + + # Если это glob паттерн, находим файлы + if [[ "$target" == *"*"* ]]; then + for file in $target; do + if [ -f "$file" ]; then + echo "Found vault file: $file" + + # Создаем копию файла в /tmp/vault_files для прямых ссылок + filename=$(basename "$file") + cp "$file" "/tmp/vault_files/$filename" + + # Расшифровываем файл если нужно + if [ -f "$VAULT_PASSWORD_FILE" ]; then + echo "Loading encrypted vault file: $file" + ansible-vault view --vault-password-file "$VAULT_PASSWORD_FILE" "$file" > "/tmp/vault_files/${filename}.decrypted" + + # Добавляем в объединенный файл с проверкой конфликтов + echo "---" >> /tmp/vault_vars.yml + echo "# From: $file" >> /tmp/vault_vars.yml + ansible-vault view --vault-password-file "$VAULT_PASSWORD_FILE" "$file" >> /tmp/vault_vars.yml + else + echo "Loading plain vault file: $file" + cp "$file" "/tmp/vault_files/${filename}.decrypted" + + # Добавляем в объединенный файл с проверкой конфликтов + echo "---" >> /tmp/vault_vars.yml + echo "# From: $file" >> /tmp/vault_vars.yml + cat "$file" >> /tmp/vault_vars.yml + fi + fi + done + else + # Обычный файл + if [ -f "$target" ]; then + echo "Found vault file: $target" + + # Создаем копию файла в /tmp/vault_files для прямых ссылок + filename=$(basename "$target") + cp "$target" "/tmp/vault_files/$filename" + + # Расшифровываем файл если нужно + if [ -f "$VAULT_PASSWORD_FILE" ]; then + echo "Loading encrypted vault file: $target" + ansible-vault view --vault-password-file "$VAULT_PASSWORD_FILE" "$target" > "/tmp/vault_files/${filename}.decrypted" + + # Добавляем в объединенный файл с проверкой конфликтов + echo "---" >> /tmp/vault_vars.yml + echo "# From: $target" >> /tmp/vault_vars.yml + ansible-vault view --vault-password-file "$VAULT_PASSWORD_FILE" "$target" >> /tmp/vault_vars.yml + else + echo "Loading plain vault file: $target" + cp "$target" "/tmp/vault_files/${filename}.decrypted" + + # Добавляем в объединенный файл с проверкой конфликтов + echo "---" >> /tmp/vault_vars.yml + echo "# From: $target" >> /tmp/vault_vars.yml + cat "$target" >> /tmp/vault_vars.yml + fi + fi + fi + done + + # Символические ссылки не нужны для работы, убираем их создание + + echo "=== VAULT VARIABLES LOADED ===" + echo "Combined vault variables:" + cat /tmp/vault_vars.yml + echo "" + echo "Individual vault files available at:" + ls -la /tmp/vault_files/ + ' + ignore_errors: true + + # ============================================================================= + # LOAD VAULT VARIABLES - Загрузка vault переменных в Ansible + # ============================================================================= + - name: Load vault variables into Ansible + include_vars: + file: /tmp/vault_vars.yml + ignore_errors: true + + - name: Set vault files path + set_fact: + vault_files_path: /tmp/vault_files + when: vault_files_path is not defined + + # ============================================================================= + # CONVERGE ЗАВЕРШЕН - Playbook'и выполняются через Makefile + # ============================================================================= + - name: Converge completed debug: msg: | ================================================================================ - CLEANUP - Перешифровка файлов после выполнения + CONVERGE ЗАВЕРШЕН ================================================================================ - Re-encrypting vault files - ================================================================================ - - - name: Post-run — re-encrypt secrets - community.docker.docker_container_exec: - container: ansible-controller - command: "bash -c 'VAULT_PASSWORD_FILE=\"/workspace/vault/.vault\"; if [ -f \"$VAULT_PASSWORD_FILE\" ] && [ -f \"/workspace/vault/secrets.yml\" ]; then ansible-vault encrypt --encrypt-vault-id default --vault-password-file \"$VAULT_PASSWORD_FILE\" /workspace/vault/secrets.yml; fi'" - ignore_errors: true \ No newline at end of file + Vault переменные загружены и готовы к использованию + Playbook'и run.yml и roles/deploy.yml будут выполнены через Makefile + ================================================================================ \ No newline at end of file diff --git a/molecule/default/create.yml b/molecule/default/create.yml index ac30afc..a6f5a79 100644 --- a/molecule/default/create.yml +++ b/molecule/default/create.yml @@ -156,7 +156,7 @@ - name: "{{ docker_network }}" privileged: "{{ systemd_defaults.privileged }}" command: "{{ '/bin/bash -c \"while true; do sleep 30; done\"' if item.family in ['alt10', 'alt9'] else systemd_defaults.command }}" - volumes: "{{ systemd_defaults.volumes | default([]) + (item.volumes | default([])) }}" + volumes: "{{ systemd_defaults.volumes | default([]) + (item.volumes | default([])) + ['/Users/inecs/PycharmProjects/DevOpsLab/vault:/workspace/vault:ro', '/Users/inecs/PycharmProjects/DevOpsLab/files:/workspace/files:ro', '/Users/inecs/PycharmProjects/DevOpsLab/roles:/workspace/roles:ro'] }}" tmpfs: "{{ systemd_defaults.tmpfs | default([]) }}" capabilities: "{{ systemd_defaults.capabilities | default([]) }}" published_ports: "{{ item.publish | default([]) }}" @@ -188,77 +188,8 @@ delay: 5 until: container_info.container.State.Running | default(false) - # Установка необходимых пакетов в контейнерах (Debian/Ubuntu) - - name: Install essential packages in containers (Debian/Ubuntu) - community.docker.docker_container_exec: - container: "{{ item.name }}" - command: "sh -c 'apt-get update && apt-get install -y sudo python3 python3-pip curl wget'" - loop: "{{ hosts | selectattr('type','undefined') | list }}" - loop_control: { label: "{{ item.name }}" } - when: item.family is defined and images[item.family] is defined and item.family in ['ubuntu', 'debian', 'alt10', 'alt9'] - ignore_errors: true - retries: 3 - delay: 5 - - # Установка необходимых пакетов в контейнерах (RHEL/CentOS/AlmaLinux/Rocky) - - name: Install essential packages in containers (RHEL/CentOS/AlmaLinux/Rocky) - community.docker.docker_container_exec: - container: "{{ item.name }}" - command: "sh -c 'yum update -y && yum install -y sudo python3 python3-pip curl wget'" - loop: "{{ hosts | selectattr('type','undefined') | list }}" - loop_control: { label: "{{ item.name }}" } - when: item.family is defined and images[item.family] is defined and item.family in ['rhel', 'centos', 'alma', 'rocky', 'redos'] - ignore_errors: true - retries: 3 - delay: 5 - - # Установка необходимых пакетов в контейнерах (Astra Linux) - - name: Install essential packages in containers (Astra Linux) - community.docker.docker_container_exec: - container: "{{ item.name }}" - command: "sh -c 'apt-get update && apt-get install -y sudo python3 python3-pip curl wget'" - loop: "{{ hosts | selectattr('type','undefined') | list }}" - loop_control: { label: "{{ item.name }}" } - when: item.family is defined and images[item.family] is defined and item.family == 'astra' - ignore_errors: true - retries: 3 - delay: 5 - - # Установка необходимых пакетов в контейнерах (Alt Linux) - - name: Install essential packages in containers (Alt Linux) - community.docker.docker_container_exec: - container: "{{ item.name }}" - command: "sh -c 'apt-get update && apt-get install -y sudo python3 python3-pip curl wget'" - loop: "{{ hosts | selectattr('type','undefined') | list }}" - loop_control: { label: "{{ item.name }}" } - when: item.family is defined and images[item.family] is defined and item.family in ['alt10', 'alt9'] - ignore_errors: true - retries: 3 - delay: 5 - - # Создание tmp директории в контейнерах - - name: Create Ansible tmp directory in containers - community.docker.docker_container_exec: - container: "{{ item.name }}" - command: "mkdir -p /tmp/.ansible-tmp && chmod 755 /tmp/.ansible-tmp" - loop: "{{ hosts | selectattr('type','undefined') | list }}" - loop_control: { label: "{{ item.name }}" } - when: item.family is defined and images[item.family] is defined - ignore_errors: true - retries: 5 - delay: 3 - - # Создание vault директории в контейнерах - - name: Create vault directory in containers - community.docker.docker_container_exec: - container: "{{ item.name }}" - command: "mkdir -p /workspace/vault && chmod 755 /workspace/vault" - loop: "{{ hosts | selectattr('type','undefined') | list }}" - loop_control: { label: "{{ item.name }}" } - when: item.family is defined and images[item.family] is defined - ignore_errors: true - retries: 5 - delay: 3 + # Примечание: Установка пакетов и создание директорий перенесены в run.yml + # для выполнения на всех поднятых контейнерах # ============================================================================= # DIND NODES - Создание контейнеров Docker-in-Docker @@ -308,7 +239,7 @@ - name: "{{ docker_network }}" privileged: "{{ systemd_defaults.privileged }}" command: "{{ systemd_defaults.command }}" - volumes: "{{ (systemd_defaults.volumes | default([])) + ['/var/run/docker.sock:/var/run/docker.sock'] + (item.volumes | default([])) }}" + volumes: "{{ (systemd_defaults.volumes | default([])) + ['/var/run/docker.sock:/var/run/docker.sock'] + (item.volumes | default([])) + ['/Users/inecs/PycharmProjects/DevOpsLab/vault:/workspace/vault:ro', '/Users/inecs/PycharmProjects/DevOpsLab/files:/workspace/files:ro', '/Users/inecs/PycharmProjects/DevOpsLab/roles:/workspace/roles:ro'] }}" tmpfs: "{{ systemd_defaults.tmpfs | default([]) }}" capabilities: "{{ systemd_defaults.capabilities | default([]) }}" published_ports: "{{ item.publish | default([]) }}" diff --git a/molecule/default/destroy.yml b/molecule/default/destroy.yml index e24c4ae..8085d0b 100644 --- a/molecule/default/destroy.yml +++ b/molecule/default/destroy.yml @@ -17,6 +17,11 @@ family: debian groups: [test] kind_clusters: [] + + # перечисли файлы/глобы, которые нужно временно расшифровать + vault_targets: + - /workspace/vault/secrets.yml + - /workspace/vault/secret.yml tasks: # ============================================================================= @@ -36,6 +41,63 @@ when: preset_file is file ignore_errors: true + # ============================================================================= + # VAULT CLEANUP - Перешифровка файлов перед удалением контейнеров + # ============================================================================= + - name: Vault cleanup operations + debug: + msg: | + ================================================================================ + VAULT CLEANUP - Перешифровка файлов перед удалением контейнеров + ================================================================================ + Re-encrypting vault files + ================================================================================ + + - name: Re-encrypt all vault files + community.docker.docker_container_exec: + container: ansible-controller + command: | + bash -c ' + VAULT_TARGETS_JSON="{{ vault_targets | to_json }}" + VAULT_PASSWORD_FILE="/workspace/vault/.vault" + + echo "=== RE-ENCRYPTING ALL VAULT FILES ===" + + if [ ! -f "$VAULT_PASSWORD_FILE" ]; then + echo "Vault password file not found: $VAULT_PASSWORD_FILE" + exit 0 + fi + + # Парсим JSON массив и перешифровываем каждый файл + echo "$VAULT_TARGETS_JSON" | jq -r ".[]" | while read -r target; do + echo "Processing target: $target" + + # Если это glob паттерн, находим файлы + if [[ "$target" == *"*"* ]]; then + for file in $target; do + if [ -f "$file" ] && ! grep -q "ANSIBLE_VAULT" "$file"; then + echo "Re-encrypting file: $file" + ansible-vault encrypt --encrypt-vault-id default --vault-password-file "$VAULT_PASSWORD_FILE" "$file" + fi + done + else + # Обычный файл + if [ -f "$target" ] && ! grep -q "ANSIBLE_VAULT" "$target"; then + echo "Re-encrypting file: $target" + ansible-vault encrypt --encrypt-vault-id default --vault-password-file "$VAULT_PASSWORD_FILE" "$target" + fi + fi + done + + echo "All vault files re-encrypted successfully" + + # Очистка символических ссылок в vault/ + echo "Cleaning up vault symlinks..." + rm -f /workspace/vault/*.decrypted + echo "Vault symlinks cleaned up" + ' + ignore_errors: true + # ============================================================================= # УДАЛЕНИЕ КОНТЕЙНЕРОВ - Остановка и удаление контейнеров # ============================================================================= @@ -114,6 +176,18 @@ vars: # Используем переменную hosts из загруженного пресета hosts: "{{ hosts }}" + # ============================================================================= + # ДОПОЛНИТЕЛЬНАЯ ОЧИСТКА - Удаление симлинков vault + # ============================================================================= + - name: Clean up vault symlinks + file: + path: "{{ item }}" + state: absent + loop: + - /workspace/vault/secrets.yml.decrypted + - /workspace/vault/secret.yml.decrypted + ignore_errors: true + - name: Display cleanup summary debug: msg: | diff --git a/molecule/default/site.yml b/molecule/default/run.yml similarity index 68% rename from molecule/default/site.yml rename to molecule/default/run.yml index 78d51fd..a169b13 100644 --- a/molecule/default/site.yml +++ b/molecule/default/run.yml @@ -24,6 +24,18 @@ tags: - setup - color-reset + + # Отладочная информация о vault переменных (передаются из converge.yml) + - name: Проверка vault переменных + debug: + msg: | + Vault переменные на {{ ansible_hostname }}: + - vault_devops_password: {{ vault_devops_password | default('НЕ ОПРЕДЕЛЕНА') | length }} символов + - vault_devops_ssh_public_key: {{ vault_devops_ssh_public_key | default('НЕ ОПРЕДЕЛЕНА') | length }} символов + tags: + - setup + - vault + - debug # Создание tmp директории для Ansible - name: Create Ansible tmp directory file: @@ -35,6 +47,18 @@ tags: - setup - tmp + + # Создание vault директории + - name: Create vault directory + file: + path: /workspace/vault + state: directory + mode: '0755' + owner: root + group: root + tags: + - setup + - vault # Обновление кеша пакетов для Debian/Ubuntu - name: Update package cache (Debian/Ubuntu) apt: @@ -76,65 +100,67 @@ - setup - update - # Установка common tools для всех ОС (ЗАКОММЕНТИРОВАНО) - # - name: Install common tools (Debian/Ubuntu) - # apt: - # name: - # - curl - # - jq - # - ca-certificates - # - iproute2 - # - iputils-ping - # - procps - # - net-tools - # - vim - # - wget - # - unzip - # - git - # state: present - # update_cache: false - # when: ansible_os_family == 'Debian' - # tags: - # - setup - # - tools + # Установка common tools для всех ОС + - name: Install common tools (Debian/Ubuntu) + apt: + name: + - curl + - jq + - ca-certificates + - iproute2 + - iputils-ping + - procps + - net-tools + - vim + - wget + - unzip + - git + - sudo + state: present + update_cache: false + when: ansible_os_family == 'Debian' + tags: + - setup + - tools - # - name: Install common tools (RHEL/CentOS/AlmaLinux/Rocky) - # yum: - # name: - # - curl - # - jq - # - ca-certificates - # - iproute - # - iputils - # - procps-ng - # - net-tools - # - vim - # - wget - # - unzip - # - git - # state: present - # when: ansible_os_family == 'RedHat' - # tags: - # - setup - # - tools + - name: Install common tools (RHEL/CentOS/AlmaLinux/Rocky) + yum: + name: + - curl + - jq + - ca-certificates + - iproute + - iputils + - procps-ng + - net-tools + - vim + - wget + - unzip + - git + - sudo + state: present + when: ansible_os_family == 'RedHat' + tags: + - setup + - tools - # - name: Install common tools (Alt Linux) - # command: apt-get install -y curl jq ca-certificates iproute2 iputils procps net-tools vim wget unzip git - # when: ansible_os_family == 'Altlinux' - # changed_when: false - # failed_when: false - # tags: - # - setup - # - tools + - name: Install common tools (Alt Linux) + command: apt-get install -y curl jq ca-certificates iproute2 iputils procps net-tools vim wget unzip git sudo + when: ansible_os_family == 'Altlinux' + changed_when: false + failed_when: false + tags: + - setup + - tools - # - name: Install common tools (Astra Linux) - # command: apt-get install -y curl jq ca-certificates iproute2 iputils procps net-tools vim wget unzip git - # when: ansible_os_family == 'Astra Linux' - # changed_when: false - # failed_when: false - # tags: - # - setup - # - tools + - name: Install common tools (Astra Linux) + command: apt-get install -y curl jq ca-certificates iproute2 iputils procps net-tools vim wget unzip git sudo + when: ansible_os_family == 'Astra Linux' + changed_when: false + failed_when: false + tags: + - setup + - tools # Установка Python для Ansible (если не установлен) - name: Install Python (Debian/Ubuntu) @@ -149,18 +175,6 @@ - setup - python - # Установка Python 3.8+ для RHEL/CentOS/Rocky/AlmaLinux - - name: Install Python 3.8+ (RHEL/CentOS/Rocky/AlmaLinux) - yum: - name: - - python3 - - python3-pip - state: present - when: ansible_os_family == 'RedHat' - tags: - - setup - - python - - name: Install Python (RHEL/CentOS/AlmaLinux/Rocky) yum: name: @@ -224,4 +238,3 @@ # - setup # - directory -- import_playbook: ../../roles/deploy.yml diff --git a/roles/deploy.yml b/roles/deploy.yml index 073af4a..a2ac2b2 100644 --- a/roles/deploy.yml +++ b/roles/deploy.yml @@ -15,17 +15,17 @@ tags: - color-reset -#- name: Установка роли devops -# hosts: all -# become: true -# roles: -# - devops - -- name: Установка роли python +- name: Установка роли devops hosts: all become: true roles: - - python + - devops + +#- name: Установка роли python +# hosts: all +# become: true +# roles: +# - python #- name: Установка роли docker # hosts: all diff --git a/roles/devops/README.md b/roles/devops/README.md index f003366..a39fa48 100644 --- a/roles/devops/README.md +++ b/roles/devops/README.md @@ -149,16 +149,16 @@ ```bash # Только создание пользователя и группы -ansible-playbook -i inventory site.yml --tags "user,group" +ansible-playbook -i inventory run.yml --tags "user,group" # Только настройка SSH -ansible-playbook -i inventory site.yml --tags "ssh,keys" +ansible-playbook -i inventory run.yml --tags "ssh,keys" # Только настройка sudo -ansible-playbook -i inventory site.yml --tags "sudo,permissions" +ansible-playbook -i inventory run.yml --tags "sudo,permissions" # Пропустить проверки -ansible-playbook -i inventory site.yml --skip-tags "verification" +ansible-playbook -i inventory run.yml --skip-tags "verification" ``` ## Обработчики diff --git a/roles/devops/defaults/main.yml b/roles/devops/defaults/main.yml index f35e0a3..5809e9b 100644 --- a/roles/devops/defaults/main.yml +++ b/roles/devops/defaults/main.yml @@ -9,11 +9,11 @@ devops_group: "devops" devops_home: "/home/{{ devops_user }}" devops_shell: "/bin/bash" -# Настройки пароля (берется из vault/secrets.yml) -devops_password: "{{ vault_devops_password | default('') }}" +# Настройки пароля (обязательно из vault/secrets.yml) +devops_password: "{{ vault_devops_password }}" -# Настройки SSH ключа (берется из vault/secrets.yml) -devops_ssh_public_key: "{{ vault_devops_ssh_public_key | default('') }}" +# Настройки SSH ключа (обязательно из vault/secrets.yml) +devops_ssh_public_key: "{{ vault_devops_ssh_public_key }}" # Настройки sudo devops_sudo_nopasswd: true diff --git a/roles/devops/tasks/main.yml b/roles/devops/tasks/main.yml index d366f88..d116595 100644 --- a/roles/devops/tasks/main.yml +++ b/roles/devops/tasks/main.yml @@ -12,25 +12,67 @@ - name: "🔍 Проверка входных параметров" tags: [devops, validation] block: - - name: "Установка значений по умолчанию для тестирования" - set_fact: - devops_password: "{{ vault_devops_password | default('123123') }}" - devops_ssh_public_key: "{{ vault_devops_ssh_public_key | default('ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbqajDhA... devops@devops.org.ru') }}" - when: devops_password == "" or devops_ssh_public_key == "" - - name: "Проверка наличия пароля пользователя devops" fail: - msg: "Пароль пользователя devops не найден в vault/secrets.yml. Установите переменную vault_devops_password." - when: - - vault_file_path is defined - - devops_password == "" + msg: | + ❌ ОШИБКА: Пароль пользователя devops не найден в vault/secrets.yml! + + Для корректной работы роли необходимо установить переменную vault_devops_password в файле vault/secrets.yml. + + Пример содержимого vault/secrets.yml: + vault_devops_password: "ваш_пароль_здесь" + vault_devops_ssh_public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbqajDhA... devops@devops.org.ru" + + Для шифрования файла используйте: + ansible-vault encrypt vault/secrets.yml + when: + - vault_devops_password is not defined + - vault_devops_password == "" + - vault_devops_password is none - name: "Проверка наличия SSH публичного ключа" fail: - msg: "SSH публичный ключ не найден в vault/secrets.yml. Установите переменную vault_devops_ssh_public_key." - when: - - vault_file_path is defined - - devops_ssh_public_key == "" + msg: | + ❌ ОШИБКА: SSH публичный ключ не найден в vault/secrets.yml! + + Для корректной работы роли необходимо установить переменную vault_devops_ssh_public_key в файле vault/secrets.yml. + + Пример содержимого vault/secrets.yml: + vault_devops_password: "ваш_пароль_здесь" + vault_devops_ssh_public_key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7vbqajDhA... devops@devops.org.ru" + + Для шифрования файла используйте: + ansible-vault encrypt vault/secrets.yml + when: + - vault_devops_ssh_public_key is not defined + - vault_devops_ssh_public_key == "" + - vault_devops_ssh_public_key is none + + - name: "✅ Проверка успешна - все необходимые секреты найдены" + debug: + msg: | + ✅ Все необходимые секреты найдены в vault/secrets.yml: + - Пароль пользователя: {{ 'установлен' if vault_devops_password is defined and vault_devops_password != '' else 'НЕ УСТАНОВЛЕН' }} + - SSH публичный ключ: {{ 'установлен' if vault_devops_ssh_public_key is defined and vault_devops_ssh_public_key != '' else 'НЕ УСТАНОВЛЕН' }} + when: + - vault_devops_password is defined + - vault_devops_password != "" + - vault_devops_ssh_public_key is defined + - vault_devops_ssh_public_key != "" + + - name: "🔍 Проверка что пароль не пустой" + fail: + msg: "❌ ОШИБКА: Пароль пользователя devops не может быть пустым!" + when: + - vault_devops_password is defined + - vault_devops_password == "" + + - name: "🔍 Проверка что SSH ключ не пустой" + fail: + msg: "❌ ОШИБКА: SSH публичный ключ не может быть пустым!" + when: + - vault_devops_ssh_public_key is defined + - vault_devops_ssh_public_key == "" - name: "Логирование начала выполнения роли" debug: @@ -51,12 +93,6 @@ name: "{{ devops_packages_to_install }}" state: present become: true - register: package_install_result - - - name: "Логирование установки пакетов" - debug: - msg: "Установлены пакеты: {{ package_install_result.changed_packages | default('нет изменений') }}" - when: false - name: "👤 Создание группы devops" tags: [devops, group] diff --git a/roles/devops/vars/main.yml b/roles/devops/vars/main.yml index 23de506..b46ab4b 100644 --- a/roles/devops/vars/main.yml +++ b/roles/devops/vars/main.yml @@ -235,6 +235,7 @@ devops_system_checks: path: "{{ devops_sudoers_file }}" register: "devops_sudoers_check" + # Настройки для логирования devops_log_config: level: "{{ devops_log_level }}" diff --git a/scripts/test-custom-images.sh b/scripts/test-custom-images.sh deleted file mode 100755 index cbe695b..0000000 --- a/scripts/test-custom-images.sh +++ /dev/null @@ -1,243 +0,0 @@ -#!/bin/bash -# Скрипт для тестирования собственных образов DevOpsLab -# Автор: Сергей Антропов -# Сайт: https://devops.org.ru - -set -e - -# Цвета для вывода -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -# Функция для вывода сообщений -log() { - echo -e "${BLUE}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1" -} - -success() { - echo -e "${GREEN}✅ $1${NC}" -} - -warning() { - echo -e "${YELLOW}⚠️ $1${NC}" -} - -error() { - echo -e "${RED}❌ $1${NC}" -} - -# Проверка наличия Docker -check_docker() { - log "Проверка Docker..." - if ! command -v docker &> /dev/null; then - error "Docker не установлен!" - exit 1 - fi - success "Docker доступен" -} - -# Проверка наличия образов -check_images() { - log "Проверка наличия собственных образов..." - - local images=( - "inecs/ansible-lab:ansible-controller-latest" - "inecs/ansible-lab:alt9-latest" - "inecs/ansible-lab:astra-linux-latest" - "inecs/ansible-lab:redos-latest" - "inecs/ansible-lab:rhel-latest" - "inecs/ansible-lab:centos-latest" - "inecs/ansible-lab:alma-latest" - "inecs/ansible-lab:rocky-latest" - "inecs/ansible-lab:ubuntu-latest" - "inecs/ansible-lab:debian-latest" - ) - - local missing_images=() - - for image in "${images[@]}"; do - if ! docker image inspect "$image" &> /dev/null; then - missing_images+=("$image") - else - success "Образ $image найден" - fi - done - - if [ ${#missing_images[@]} -gt 0 ]; then - warning "Отсутствующие образы:" - for image in "${missing_images[@]}"; do - echo " - $image" - done - echo "" - echo "Для сборки образов выполните:" - echo " make docker build" - echo "" - echo "Или соберите отдельные образы:" - for image in "${missing_images[@]}"; do - # Маппинг образов на имена для сборки - case "$image" in - "inecs/ansible-lab:ansible-controller-latest") - echo " make docker build-image IMAGE=ansible-controller" - ;; - "inecs/ansible-lab:alt9-latest") - echo " make docker build-image IMAGE=alt9" - ;; - "inecs/ansible-lab:astra-linux-latest") - echo " make docker build-image IMAGE=astra-linux" - ;; - "inecs/ansible-lab:redos-latest") - echo " make docker build-image IMAGE=redos" - ;; - "inecs/ansible-lab:rhel-latest") - echo " make docker build-image IMAGE=rhel" - ;; - "inecs/ansible-lab:centos-latest") - echo " make docker build-image IMAGE=centos" - ;; - "inecs/ansible-lab:alma-latest") - echo " make docker build-image IMAGE=alma" - ;; - "inecs/ansible-lab:rocky-latest") - echo " make docker build-image IMAGE=rocky" - ;; - "inecs/ansible-lab:ubuntu-latest") - echo " make docker build-image IMAGE=ubuntu" - ;; - "inecs/ansible-lab:debian-latest") - echo " make docker build-image IMAGE=debian" - ;; - *) - echo " # Неизвестный образ: $image" - ;; - esac - done - return 1 - fi - - success "Все образы найдены" - return 0 -} - -# Тестирование с минимальным пресетом -test_minimal() { - log "Тестирование с минимальным пресетом (custom-minimal)..." - - if [ -f "molecule/presets/custom-minimal.yml" ]; then - log "Запуск molecule test с пресетом custom-minimal..." - if molecule test --scenario-name custom-minimal; then - success "Тест с custom-minimal прошел успешно" - else - error "Тест с custom-minimal завершился с ошибкой" - return 1 - fi - else - error "Файл molecule/presets/custom-minimal.yml не найден" - return 1 - fi -} - -# Тестирование с полным пресетом -test_full() { - log "Тестирование с полным пресетом (custom-images)..." - - if [ -f "molecule/presets/custom-images.yml" ]; then - log "Запуск molecule test с пресетом custom-images..." - if molecule test --scenario-name custom-images; then - success "Тест с custom-images прошел успешно" - else - error "Тест с custom-images завершился с ошибкой" - return 1 - fi - else - error "Файл molecule/presets/custom-images.yml не найден" - return 1 - fi -} - -# Тестирование производительности -test_performance() { - log "Тестирование производительности (custom-performance)..." - - if [ -f "molecule/presets/custom-performance.yml" ]; then - log "Запуск molecule test с пресетом custom-performance..." - if molecule test --scenario-name custom-performance; then - success "Тест производительности прошел успешно" - else - error "Тест производительности завершился с ошибкой" - return 1 - fi - else - error "Файл molecule/presets/custom-performance.yml не найден" - return 1 - fi -} - -# Очистка после тестов -cleanup() { - log "Очистка после тестов..." - - # Остановка и удаление контейнеров - docker ps -a --filter "name=molecule" --format "{{.Names}}" | xargs -r docker rm -f 2>/dev/null || true - - # Удаление сетей - docker network ls --filter "name=ansible-lab" --format "{{.Name}}" | xargs -r docker network rm 2>/dev/null || true - - success "Очистка завершена" -} - -# Основная функция -main() { - log "🚀 Тестирование собственных образов DevOpsLab" - echo "==========================================" - - # Проверки - check_docker - - if ! check_images; then - error "Не все образы найдены. Завершение." - exit 1 - fi - - # Выбор типа тестирования - case "${1:-minimal}" in - "check") - success "Проверка образов завершена" - ;; - "minimal") - test_minimal - ;; - "full") - test_full - ;; - "performance") - test_performance - ;; - "all") - test_minimal && test_full && test_performance - ;; - *) - echo "Использование: $0 [check|minimal|full|performance|all]" - echo "" - echo " check - проверка наличия образов" - echo " minimal - минимальный тест (4 хоста)" - echo " full - полный тест (все образы)" - echo " performance - тест производительности (8 хостов)" - echo " all - все тесты" - exit 1 - ;; - esac - - # Очистка - cleanup - - success "🎉 Тестирование завершено успешно!" -} - -# Обработка сигналов -trap cleanup EXIT INT TERM - -# Запуск -main "$@" diff --git a/scripts/update-playbooks.sh b/scripts/update-playbooks.sh index bf0b4a5..b277296 100755 --- a/scripts/update-playbooks.sh +++ b/scripts/update-playbooks.sh @@ -12,10 +12,10 @@ ROLES=$(find roles/ -name 'main.yml' -path '*/tasks/*' | sed 's|roles/||; s|/tas echo "📋 Найденные роли: $ROLES" -# Обновляем molecule/default/site.yml (только если файл не существует) -if [ ! -f "molecule/default/site.yml" ]; then - echo "📝 Создание molecule/default/site.yml..." - cat > molecule/default/site.yml << 'EOF' +# Обновляем molecule/default/run.yml (только если файл не существует) +if [ ! -f "molecule/default/run.yml" ]; then + echo "📝 Создание molecule/default/run.yml..." + cat > molecule/default/run.yml << 'EOF' --- # Универсальный playbook для тестирования Ansible ролей # Автор: Сергей Антропов @@ -199,7 +199,7 @@ if [ ! -f "molecule/default/site.yml" ]; then - import_playbook: ../../roles/deploy.yml EOF else - echo "📝 Файл molecule/default/site.yml уже существует, пропускаем создание" + echo "📝 Файл molecule/default/run.yml уже существует, пропускаем создание" fi # Обновляем roles/deploy.yml diff --git a/vault/secrets.yml b/vault/secrets.yml index 6a52805..109edc9 100644 --- a/vault/secrets.yml +++ b/vault/secrets.yml @@ -1,51 +1,51 @@ $ANSIBLE_VAULT;1.1;AES256 -34663739653463366234613631353064393733363234323430333465393362666165373562303565 -3037663539383033313563623635393536366438316433320a623361306663373332326165326633 -37336136343965616234663462333165363965323362326266373566303238303033663336333331 -3435343232613563630a626639343461613239643532663138303630373739623836326238646236 -35346264346231353364363065393266623934323964363163623964643162306461633431616233 -62663339353134616130623862383332343037363739366237383638373638366363306231663635 -35373638346338386366636235616366323433636164333434313831613465343335366237366131 -65626562373532623263373430383531313631613638643466636661613763316465313833643030 -66346130393131383037386464353239663232636561666638333937346236613438616265313363 -65373766363866386466613035373266663235656537336431643931373131333435383761663937 -33626230343833656162623263363230383362393233393166346661303162353762613433376663 -61346236666338646534656138363030313562346465346563613266653930623532353765326234 -38373562613438653364323465363337363066313638373232643439386137623136313066306362 -39386437353233366563613962653837383664343462666537323335373839383334346364306361 -64366362396333643830313766373330613832323739306530333664333737343239393964643831 -34373736663132383134343334353234633061303335363364333339386331663962363334316231 -30663438376632326334383231363737333534633939316363366436633266323665363062343431 -37616137393932626433623962636537376238326466326136396532636666643234323639316235 -36306137656431626162343231616530663637323835346139323734393066623039396466346264 -62656161366663303838383661623966373265393130616431663331636234626231343632333831 -62313037336261313939636636626133656136366161346439663530373530383338393764366239 -63626634303866636636636366323539326534363461306333663638613261313164373961633866 -33656632633465663431643938313035303366376536343434333231353932303065306662393933 -37643134306535303036363130356435633039616637653363303164376335393665386334353664 -64366562353933633063353166666636333563613562386632616163376166626462393261376438 -37306131666232633330656464613133313032303333623735326439616166323765393766393036 -63336538336166633534643063353864636131353232393433313961333234386337616139613133 -65303666393431623031373966396632333536613664363239616236623338306336313331313062 -36346338326161653664646537656538626535613561353739396134323537666563386136343238 -66396666323234316237656137306666376439633261376233366234323834383963313138666366 -31653238303633353164636263643934326466636664386265383762323138333466323332643732 -62313462316431383061663166363834666234386163393030643265336333316232626561353031 -61353639333435303237666232643830336530353735656137333338643730343835346264306265 -61373662623863323430656166376338306463373835333661353466653261633636383764363366 -38323835666531633233326264323330616139313431353763343061393661333038363864383863 -62323239316135643231626661663237636466653763636433613039633661386531343161613838 -64383834663530636530363133396431383738636638333661636239643264623365353564653964 -64376131323239663633303662333438636431313262316639363531323930396238646665343262 -38393764643062343531326365393536653862653735393035623465386134633163323635356530 -38303365376561666133663639376234323363356264336566666565383933373330356562643063 -34663832613339376232663561323261653937333339313862613164656436323239623239613664 -66383131613434366133663833663734313437376461663530366166323361643566393835626334 -35383437376663326232336630386662386435663933303635616431376463333461316262666663 -63393330656665356138613663333565616230366338396361333265396562306438613263363035 -63626662396131623431353462376364656265373363383737303034363336313330656663626161 -33396161373730303239623338323564386139333838393661323466363035616635663765316238 -61636563356665616435653064316334633530643731366239383530346532336366666230613730 -38613566346131303863663463656534306530383364383031653964333939353536633262636635 -37643662323333356639663363636430626361343830663261623234343161643733633130613534 -3039653530613936353962373738396265656264636263656333 +35646637393562393632386237323463396437383465313133653161646465393066623139613931 +3731306562663437326132393337313232373935613636320a393162333031623336383230353934 +33323166643666323037353261663264663665333062306138346234353839383439323964643137 +3762616565326465330a663932393563656532316430356266303761333230626133333363336234 +63373562326330373737373265613438653135393039303665333236303533643365356464306436 +38376531313765336266353037666665623535313162353061366463643964343966333035646236 +64363833326266376162306132363330396565306264316233663665376630643563333234346433 +39663735303837383339613732333865633333373465656164623235313938373039643636636537 +35356164313534323662333062383536323961313837353632663666346537633530326566373632 +35346266336536396363303262326362346436336564613635376566643235373638373636383164 +64396137383231363637653235333066366462666335316433363363656337353632333665313866 +38313837356538633465653539316563393965373163323166633737623963666531323532303037 +39336330363338623134326163313034656265623539656565316361656437653463366165373137 +32383735323430313531663139613165303865633435373664633866656631353763613338303934 +34613461663665643965303833636131346462613236616162393861633465313866363831313332 +65336335663735666634396135383038343436336138636137313135383961306461323831366233 +66643766316339626639623036623835336361336231646161616437643733363636656433633130 +32306537366462633030313036333331613066376235613835636236636564663164346263376161 +39343837613134663537373461323239616236656633613637396261386335613437396164653664 +38346564646432396634343430636664346435396138326236613133323930613531643730396231 +34386166303031303239356565323566376565653139393265656463356631303831306136333865 +36623732643463303264653735636664663130373233336162633533653338343735313932626562 +64366233346237313230636263666561636462636139656461333432333036653036666132623932 +65623033636333336533623437396539383932633438346132633339363532323536316338626339 +38326237306661666637613064613435613931383265653761303734663832616434336430393535 +33663335343937633135326530386164316631626162353138323132376538383437353862363137 +38393638643066643335396566633239373338373531646334323831653832326532323464653834 +39666264623033356635646433343631366335643764323465323737396262323932623634613635 +65316536356662306533663839623133383263363630373766666464663063323035323834636539 +63666137313935373366326565393830353837396463363330383534373331363539386463316266 +35363734353262313366376530303231306133623465373133616139613031363230333432343265 +38313637303336343238376466383737646135653964346564363432666462373665383662656534 +34333234346133356430306232313430613739663464656336623739333330353961393866326634 +62623633343439366133643534393237616138383235656237343836646464303130633637663430 +33356631633363363535663139323330633533353465323234323238386132643032626664326431 +34663862363830393465333662376563373932356363303432383461653464366633623538346263 +32393431333931333963343363653935333662326632373463356566316537383739363064343862 +37633466656364376332336434633266346465656261303736363864366237663535313566313334 +34313734303137323163636636386238306335323561653166393335316563326661646566353662 +39383861643264363366393434633236613564353232393631393266386463613433376561383763 +30303262666630663138306237613939356138613330613431386236646338366434346330363736 +62386535376164326566346365313133616265643035643861613635333730613438636430393565 +66643864653634303139353434336564376562613933346138393534646532663763633363626535 +38336532303338656134363665323030623064626261376237626265656332343036336638313733 +35386438383561393636383536326262343931343530366339613332323066386366616165303033 +64306139616432323837623466393738323865306135393064626665326139626439313332636132 +63616664613732366338656236313439326336633930303133333330313761633132326362616537 +62666634343937613136633962383331666361353036663839633733623533623466383432313462 +34393733636432343365363862393663653163353534633136333661363363313139393330373930 +6163336530313937363666636633313066333931336138643164