commit 8242af3182c8439ece58f0c7edf4e712eafa9a4e Author: Sergey Antropoff Date: Mon Mar 31 21:33:34 2025 +0300 First commit diff --git a/.ansible-lint b/.ansible-lint new file mode 100644 index 0000000..a14585e --- /dev/null +++ b/.ansible-lint @@ -0,0 +1,7 @@ +skip_list: + - fqcn + - yaml[new-line-at-end-of-file] + - yaml[truthy] + - yaml[line-length] + - var-naming[no-role-prefix] + - 'ignore-errors' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61671ed --- /dev/null +++ b/.gitignore @@ -0,0 +1,173 @@ +# ---> Ansible +*.retry + +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..2de4968 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,88 @@ +stages: + - lint +# - test + - deploy + - notify + +services: + - name: docker:dind + command: ["--tls=false"] + +variables: + DOCKER_IMAGE: "hub.antropoff.ru/ansible/ansible:latest" + DOCKER_TLS_CERTDIR: "" + ANSIBLE_FORCE_COLOR: "true" + +before_script: + - echo "$CI_REGISTRY_PASSWORD" | docker login hub.antropoff.ru -u "$CI_REGISTRY_USER" --password-stdin + - docker pull $DOCKER_IMAGE + - echo "Fixing directory permissions..." + - chmod o-w $CI_PROJECT_DIR + +lint: + stage: lint + script: + - echo "Начинаем стейдж Lint" + - echo "Распаковываем секреты..." + - ansible-vault decrypt vars/secrets.yml --vault-password-file ./vault-password.txt + - echo "Запускаем ansible-lint..." + - ansible-lint roles/* + - echo "Упаковываем секреты..." + - ansible-vault encrypt vars/secrets.yml --encrypt-vault-id default --vault-password-file ./vault-password.txt + allow_failure: false + rules: + - if: $CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "master" + +#test: +# stage: test +# script: +# - echo "Распаковываем секреты..." +# - ansible-vault decrypt --vault-password-file ./vault-password.txt vars/secrets.yml +# - echo "Запускаем тесты через Молекулу..." +# - molecule test --parallel --destroy=always +# - echo "Упаковываем секреты..." +# - ansible-vault encrypt vars/secrets.yml --encrypt-vault-id default --vault-password-file ./vault-password.txt +# allow_failure: false +# rules: +# - if: $CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "master" + +deploy: + stage: deploy + script: + - echo "Настраиваем SSH-ключ для доступа к серверам..." + # Создаем директорию .ssh и настраиваем права доступа + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + # Записываем SSH-ключ в файл ~/.ssh/id_rsa + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + # Запускаем основной пайплайн + - echo "Распаковываем секреты..." + - ansible-vault decrypt --vault-password-file ./vault-password.txt vars/secrets.yml + - echo "Все ок. Деплоим в прод..." + - ansible-playbook roles/deploy.yaml + - echo "Упаковываем секреты..." + - ansible-vault encrypt vars/secrets.yml --encrypt-vault-id default --vault-password-file ./vault-password.txt + # Удаляем ключ + - rm -rf ~/.ssh + rules: + - if: $CI_COMMIT_REF_NAME != "main" && $CI_COMMIT_REF_NAME != "master" + when: manual + +notify: + stage: notify + script: + - | + if [ "$CI_JOB_STATUS" == "success" ]; then + MESSAGE="✅ Настройки кластера успешно завершены!%0AПроект: $CI_PROJECT_NAME%0AВетка: $CI_COMMIT_REF_NAME%0AСтатус: $CI_JOB_STATUS" + else + MESSAGE="❌ Настройки кластера были произведены с ошибкой!%0AПроект: $CI_PROJECT_NAME%0AВетка: $CI_COMMIT_REF_NAME%0AСтатус: $CI_JOB_STATUS" + fi +# curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_BOT_TOKEN/sendMessage" \ +# -d "chat_id=$TELEGRAM_CHAT_ID" \ +# -d "text=$MESSAGE" + rules: + - if: $CI_JOB_STATUS # Отправлять уведомление только после завершения пайплайна + +after_script: + - echo "Работа пайплайна завершена" diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/AnsibleTemplate.iml b/.idea/AnsibleTemplate.iml new file mode 100644 index 0000000..db32fb1 --- /dev/null +++ b/.idea/AnsibleTemplate.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..38aa11a --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..95a0fdb --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,47 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a6dd401 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..febcd53 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5bef48c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# Используем более легкий базовый образ +FROM python:3.12.9-slim-bullseye + +# Добавляем метаданные +LABEL maintainer="Сергей Антропов " +LABEL description="Этот Dockerfile создан для внедрения подхода IaC в Ansible." +LABEL version="0.1" +LABEL contact.website="https://devops.org.ru" + +# Устанавливаем переменные окружения +ENV PYTHONUNBUFFERED=1 +ENV EDITOR=nano + +# Устанавливаем системные зависимости +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + ssh \ + gcc \ + libffi-dev \ + libssl-dev \ + make \ + sudo \ + sshpass \ + openssh-client \ + nano \ + less \ + ca-certificates \ + curl \ + gnupg \ + diffutils \ + jq \ + lsb-release \ + && mkdir -p /etc/apt/keyrings \ + && curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian bullseye stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null \ + && apt-get update && \ + apt-get install -y --no-install-recommends \ + docker-ce-cli \ + && rm -rf /var/lib/apt/lists/* + +# Устанавливаем зависимости Python для Ansible и Molecule +RUN pip install --upgrade pip && \ + pip install \ + ansible \ + ansible-lint \ + ansible-vault \ + molecule \ + molecule-docker \ + molecule-plugins \ + ansible-compat \ + docker + +# Копируем ssh ключ +#COPY id_rsa /root/.ssh/id_rsa +#RUN chmod 600 /root/.ssh/id_rsa + +# Устанавливаем рабочую директорию +WORKDIR /ansible + +# Команда по умолчанию +CMD ["/bin/bash"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c3ed1dc --- /dev/null +++ b/Makefile @@ -0,0 +1,151 @@ +# Глобальные переменные +IMAGE ?= ansible +TAG ?= 0.1 +REGISTRY ?= hub.antropoff.ru/ansible +# По умолчанию используем docker. Для локальной разработки используйте docker-compose +RUN_MODE ?= docker + +# Определение команды RUN в зависимости от RUN_MODE +ifeq ($(RUN_MODE), docker-compose) + RUN = docker compose run --rm $(IMAGE) +else ifeq ($(RUN_MODE), docker) + RUN = docker run -it --rm \ + --name $(IMAGE) \ + -v $(PWD):/ansible \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v ~/.ssh/id_rsa:/root/.ssh/id_rsa:ro \ + -e ANSIBLE_VAULT_PASSWORD_FILE=/ansible/vault-password.txt \ + --privileged \ + --workdir /ansible \ + $(REGISTRY)/$(IMAGE) +else + $(error Invalid RUN_MODE. Use "docker-compose" or "docker") +endif + +view create edit show delete test lint deploy new init build rebuild prune release images push pull shell: + @true + +#################################################################################################### +# Инициализация новой роли +#################################################################################################### +init: + @echo "Шаг 1: Создание Docker-образа..." + @make docker build + @echo "Шаг 2: Создание Docker-образов для запуска Molecule..." + @make docker images + @echo "Шаг 3: Создание нового vault-файла с паролем..." + @read -p "Введите пароль для vault: " VAULT_PASSWORD; \ + echo "$$VAULT_PASSWORD" > vault-password.txt; \ + make vault create + @echo "Шаг 4: Создание нового брэнча в гите..." + @make git new + @echo "Шаг 5: Создание новой роли..." + @make role new + +#################################################################################################### +# Управление контейнерами с помощью docker compose или docker run +#################################################################################################### +docker: + @case "$(word 2, $(MAKECMDGOALS))" in \ + build) \ + docker buildx create --use --name multiarch-builder --driver docker-container; \ + if [ "$(RUN_MODE)" = "docker-compose" ]; then \ + docker compose build $(c); \ + else \ + docker build -t $(REGISTRY)/$(IMAGE) .; \ + fi;; \ + rebuild) \ + docker buildx create --use --name multiarch-builder --driver docker-container; \ + if [ "$(RUN_MODE)" = "docker-compose" ]; then \ + docker compose build --no-cache $(c); \ + else \ + docker build --no-cache -t $(REGISTRY)/$(IMAGE) .; \ + fi;; \ + prune) \ + docker system prune -af;; \ + shell) \ + clear; \ + echo "Entering to Ansible container shell..."; \ + $(RUN) bash ;; \ + release) \ + docker buildx create --use --name multiarch-builder --driver docker-container; \ + docker login $(REGISTRY); \ + docker buildx build -t $(REGISTRY)/$(IMAGE):$(TAG) -t $(REGISTRY)/$(IMAGE):latest --platform linux/amd64,linux/arm64 --push .;; \ + images) \ + docker buildx create --use --name multiarch-builder --driver docker-container; \ + docker login $(REGISTRY); \ + docker buildx build -t $(REGISTRY)/centos:latest --platform linux/amd64,linux/arm64 --push -f Dockerfile-CentOS .; \ + docker buildx build -t $(REGISTRY)/ubuntu:latest --platform linux/amd64,linux/arm64 --push -f Dockerfile-Ubuntu .;; \ + *) echo "Unknown action. Available actions: build, rebuild, prune, release";; \ + esac + +#################################################################################################### +# Работа с ролью +#################################################################################################### +vault: + @case "$(word 2, $(MAKECMDGOALS))" in \ + show) $(RUN) bash -c "ansible-vault view --vault-password-file vault-password.txt vars/secrets.yml";; \ + create) $(RUN) bash -c "ansible-vault create --encrypt-vault-id default --vault-password-file vault-password.txt vars/secrets.yml";; \ + edit) $(RUN) bash -c "ansible-vault edit --vault-password-file vault-password.txt vars/secrets.yml";; \ + delete) $(RUN) bash -c "rm vars/secrets.yml";; \ + rekey) $(RUN) bash -c "ansible-vault rekey --vault-password-file vault-password.txt vars/secrets.yml";; \ + decrypt) $(RUN) bash -c "ansible-vault decrypt --vault-password-file vault-password.txt vars/secrets.yml";; \ + encrypt) $(RUN) bash -c "ansible-vault encrypt --encrypt-vault-id default --vault-password-file vault-password.txt vars/secrets.yml";; \ + *) echo "Unknown action";; \ + esac + +role: + @case "$(word 2, $(MAKECMDGOALS))" in \ + new) \ + clear; \ + echo "Введите название новой роли на английском:"; \ + read ROLE_NAME; \ + echo "Введите описание роли:"; \ + read ROLE_DESC; \ + cp -r default/ "roles/$${ROLE_NAME}"; \ + printf "\n- name: $${ROLE_DESC}" >> roles/deploy.yaml; \ + printf "\n import_playbook: $${ROLE_NAME}/deploy.yaml" >> roles/deploy.yaml; \ + printf '\n - ../../roles/%s' "$$ROLE_NAME" >> molecule/default/converge.yml; \ + printf "\n - $${ROLE_NAME}" >> roles/$$ROLE_NAME/deploy.yaml;; \ + lint) \ + clear; \ + echo "Check your role..."; \ + $(RUN) bash -c "ansible-vault decrypt --vault-password-file vault-password.txt vars/secrets.yml"; \ + $(RUN) bash -c "ansible-lint roles/*"; \ + $(RUN) bash -c "ansible-vault encrypt vars/secrets.yml --encrypt-vault-id default --vault-password-file vault-password.txt";; \ + test) \ + clear; \ + echo "Running test roles..."; \ + $(RUN) bash -c "ansible-vault decrypt --vault-password-file vault-password.txt vars/secrets.yml"; \ + $(RUN) bash -c "molecule test --parallel --destroy=always"; \ + $(RUN) bash -c "ansible-vault encrypt vars/secrets.yml --encrypt-vault-id default --vault-password-file vault-password.txt";; \ + deploy) \ + clear; \ + echo "Deploying roles to production..."; \ + $(RUN) bash -c "ansible-playbook roles/deploy.yaml";; \ + *) echo "Unknown action";; \ + esac + +#################################################################################################### +# Работа с Git +#################################################################################################### +git: + @case "$(word 2, $(MAKECMDGOALS))" in \ + push) \ + git branch; \ + read -p "Выберите ветку для пуша: " BRANCH; \ + read -p "Введите описание коммита: " COMMIT; \ + commitname=$$COMMIT; \ + git add . ; \ + git commit -m "$$commitname"; \ + git push -u origin $$BRANCH; \ + echo "Изменения внесены в Git";; \ + pull) \ + git pull;; \ + new) \ + read -p "Введите имя новой ветки: " BRANCH_NAME; \ + NEW_BRANCH="$$BRANCH_NAME"; \ + git checkout -b $$NEW_BRANCH; \ + echo "Создана и переключена на новую ветку: $$NEW_BRANCH";; \ + *) echo "Unknown action. Available actions: push, pull, cluster-branch";; \ + esac diff --git a/README.md b/README.md new file mode 100644 index 0000000..fddc788 --- /dev/null +++ b/README.md @@ -0,0 +1,109 @@ +# Patroni GitOps Config Manager + +### Назначение роли +Эта роль предназначена для безопасного управления конфигурацией кластера Patroni. Она предоставляет следующие возможности: + +- Создание резервных копий текущей конфигурации с timestamp +- Валидация изменений конфигурации перед применением +- Наглядное отображение различий между текущей и новой конфигурацией +- Безопасное применение изменений через REST API Patroni +- Проверка состояния кластера после изменений +- Уведомление о необходимости перезагрузки нод (если требуется) +- Автоматическое управление историей конфигурационных файлов + +### Требования +- Ansible 2.9+ +- Доступ к Patroni REST API (порт 8008) +- Python 3 для валидации YAML +- Утилита diff с поддержкой цветного вывода + +### Переменные роли +Основные переменные, которые можно переопределить: + +- config_dir (по умолчанию: "/ansible/history") - директория для хранения истории конфигураций +- config_file (по умолчанию: "/ansible/patroni_config.yaml") - путь к файлу с изменениями конфигурации +- patroni_host (по умолчанию: "10.14.0.180") - хост кластера Patroni + +### Как внести изменения в конфиг кластера? +1. Для начала создайте новый branch по имени кластера. + ```bash + # Переключиться на основную ветку (если нужно) + git checkout main + + # Получить последние изменения + make git pull + + # Создать новую ветку + make git new + ``` +2. Сбилдите образ Ansible для локальной работы, тестов и линта. + ```bash + make docker build + ``` +3. Внесите изменения в файл **patroni_config.yaml**. Все изменения буду добавлены в конфиг кластера. +Если такие переменные в конфиге существуют, они будут заменены. Если переменных нет, будут добавлены. + + ```yaml + postgresql: + parameters: + max_connections: 100 + shared_buffers: "1GB" + use_pg_rewind: true + ``` + +3. Линт, тесты и пуш в гит + ``` + # Внести изменения в secret.yaml + make vault edit + + # Проверить роль + make role lint + + # Протестировать роль + make role test + + # Пушим в гит... + make git push + ``` + +4. Если у вас GitLab - смотрим и наслаждаемся. Не забудьте нажать кнопочку в +GitLab на deploy изменений конфига в кластере. Ну, или закомментируйте в gitlab-ci.yml строку + ```gitlab + when: manual + ``` + что бы можно было деплоить автоматом... + +### Анализ результатов +После выполнения этапа подготовки: + +- Будет показан цветной diff изменений +- В директории config_dir появятся: +- Резервная копия текущей конфигурации с timestamp +- Файл last.yaml с новой конфигурацией + +После выполнения этапа применения: + +- Будет выведен статус кластера +- Появится предупреждение, если требуется перезагрузка нод +- Старые конфигурации будут автоматически удалены (остаются последние 10) + +### Особенности работы +Безопасность: +- Конфигурация всегда сохраняется перед изменениями +- Изменения проверяются на валидность перед применением +- Показывается подробный diff перед применением + +Информирование: +- Явное предупреждение о необходимости перезагрузки +- Подробный вывод статуса кластера после изменений +- Сохранение истории изменений + +Автоматизация: +- Управление историей конфигураций (автоочистка старых файлов) +- Проверка состояния кластера после изменений + +### Важные примечания +- Роль не выполняет автоматическую перезагрузку нод Patroni, так как это потенциально опасная операция +- При необходимости перезагрузки будет показана команда для ручного выполнения +- Все изменения конфигурации сохраняются с timestamp для возможности отката +- Для работы требуется доступ к API Patroni (порт 8008) diff --git a/ansible.cfg b/ansible.cfg new file mode 100644 index 0000000..bf2dee5 --- /dev/null +++ b/ansible.cfg @@ -0,0 +1,11 @@ +[defaults] +inventory = inventory/hosts +vault_password_file = vault-password.txt +remote_user = devops +host_key_checking = False +enable_plugins = yaml, ini +roles_path = roles/ +interpreter_python = auto +stdout_callback = yaml +bin_ansible_callbacks = True +force_color = 1 diff --git a/default/defaults/.gitkeep b/default/defaults/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/default/deploy.yaml b/default/deploy.yaml new file mode 100644 index 0000000..61ab8b8 --- /dev/null +++ b/default/deploy.yaml @@ -0,0 +1,10 @@ +--- +- name: Deploy roles + hosts: all + become: true + become_user: root + become_method: ansible.builtin.sudo + gather_facts: true + vars_files: + - ../../vars/secrets.yml + roles: \ No newline at end of file diff --git a/default/files/.gitkeep b/default/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/default/handlers/.gitkeep b/default/handlers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/default/meta/.gitkeep b/default/meta/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/default/tasks/debian/main.yaml b/default/tasks/debian/main.yaml new file mode 100644 index 0000000..4c32e03 --- /dev/null +++ b/default/tasks/debian/main.yaml @@ -0,0 +1,4 @@ +--- +- name: Пример таски + debug: + msg: "Привет! Я запустился на Debian/Ubuntu!" diff --git a/default/tasks/main.yaml b/default/tasks/main.yaml new file mode 100644 index 0000000..2b9ddee --- /dev/null +++ b/default/tasks/main.yaml @@ -0,0 +1,12 @@ +--- +- name: "Определяем ОС" + set_fact: + os_family: "{{ ansible_facts['os_family'] }}" + +- name: "Подключаем таски для RedHat совместимых" + include_tasks: "redhat/main.yaml" + when: os_family == "RedHat" + +- name: "Подключаем таски для Debian/Ubuntu совместимых" + include_tasks: "debian/main.yaml" + when: os_family == "Debian" diff --git a/default/tasks/redhat/main.yaml b/default/tasks/redhat/main.yaml new file mode 100644 index 0000000..64e5c36 --- /dev/null +++ b/default/tasks/redhat/main.yaml @@ -0,0 +1,4 @@ +--- +- name: Пример таски + debug: + msg: "Привет! Я запустился на RedHat/CentOS/Fedora!" diff --git a/default/templates/.gitkeep b/default/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/default/tests/.gitkeep b/default/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/history/.gitkeep b/history/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/inventory/.gitkeep b/inventory/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/inventory/hosts b/inventory/hosts new file mode 100644 index 0000000..a4a29bb --- /dev/null +++ b/inventory/hosts @@ -0,0 +1 @@ +[all] diff --git a/molecule/default/converge.yml b/molecule/default/converge.yml new file mode 100644 index 0000000..462c2a4 --- /dev/null +++ b/molecule/default/converge.yml @@ -0,0 +1,7 @@ +--- +- name: Converge + hosts: all + vars_files: + - ../../vars/secrets.yml + roles: + - ../../roles/prepare diff --git a/molecule/default/destroy.yml b/molecule/default/destroy.yml new file mode 100644 index 0000000..82979c7 --- /dev/null +++ b/molecule/default/destroy.yml @@ -0,0 +1,8 @@ +- name: Destroy containers on interrupt + hosts: localhost + tasks: + - name: Ensure containers are destroyed + docker_container: + name: "{{ item.name }}" + state: absent + loop: "{{ molecule_yml.platforms }}" \ No newline at end of file diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml new file mode 100644 index 0000000..086fee3 --- /dev/null +++ b/molecule/default/molecule.yml @@ -0,0 +1,60 @@ +--- +dependency: + name: galaxy + enabled: true + options: + requirements-file: requirements.yml + +driver: + name: docker + +platforms: +# - name: centos +# image: "hub.antropoff.ru/ansible/centos:latest" +# privileged: true +# pre_build_image: true +# volumes: +# - /sys/fs/cgroup:/sys/fs/cgroup:ro +# - /var/run/docker.sock:/var/run/docker.sock +# tmpfs: +# - /tmp +# - /run + - name: ubuntu + image: "hub.antropoff.ru/ansible/ubuntu:latest" + privileged: true + pre_build_image: true + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro + - /var/run/docker.sock:/var/run/docker.sock + tmpfs: + - /tmp + - /run + +provisioner: + name: ansible + connection_options: + ansible_connection: docker + ansible_user: root + env: + ANSIBLE_PYTHON_INTERPRETER: /usr/bin/python3 + lint: + name: ansible-lint + +verifier: + name: ansible + +scenario: + name: default + test_sequence: + - dependency + - cleanup + - destroy + - syntax + - create + - prepare + - converge + - idempotence + - side_effect + - verify + - cleanup + - destroy diff --git a/molecule/default/no-prepare.yml b/molecule/default/no-prepare.yml new file mode 100644 index 0000000..57b792f --- /dev/null +++ b/molecule/default/no-prepare.yml @@ -0,0 +1,43 @@ +- name: Prepare + hosts: all + tasks: + - name: Detect OS family + ansible.builtin.setup: + gather_subset: + - "min" + + - name: Обновляем пакеты для работы с Ansible в RockyLinux (Centos/RedHat) + when: ansible_facts['os_family'] == "RedHat" + block: + - name: Устанавливаем репозиторий AppStream (если его нет) + ansible.builtin.raw: dnf config-manager --set-enabled appstream + changed_when: false + + - name: Установить rsync + ansible.builtin.raw: dnf install -y rsync + changed_when: false + + - name: Устанавливаем Python 3.8 + ansible.builtin.raw: dnf install -y python38 python38-pip + changed_when: false + + - name: Обновляем символическую ссылку python3 + ansible.builtin.raw: alternatives --set python /usr/bin/python3.8 + changed_when: false +# - name: Fix repository URLs +# ansible.builtin.command: +# cmd: sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-* +# changed_when: false + +# - name: Update baseurl +# ansible.builtin.command: +# cmd: sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-* +# changed_when: false + +# - name: Install required packages +# ansible.builtin.yum: +# name: +# - epel-release +# - python3 +# - python3-pip +# state: present diff --git a/molecule/default/no-verify.yml b/molecule/default/no-verify.yml new file mode 100644 index 0000000..5e80115 --- /dev/null +++ b/molecule/default/no-verify.yml @@ -0,0 +1,7 @@ +--- +- name: Prepare + hosts: all + tasks: + - name: Reun verify + debug: + msg: "Hello, Verify!" diff --git a/patroni_config.yaml b/patroni_config.yaml new file mode 100644 index 0000000..1f3d7e4 --- /dev/null +++ b/patroni_config.yaml @@ -0,0 +1,5 @@ +postgresql: + parameters: + max_connections: 300 + shared_buffers: "12GB" + use_pg_rewind: true diff --git a/requirements.yml b/requirements.yml new file mode 100644 index 0000000..cf12a33 --- /dev/null +++ b/requirements.yml @@ -0,0 +1,4 @@ +--- +collections: + - name: maxhoesel.proxmox + version: 5.0.1 diff --git a/roles/apply/defaults/.gitkeep b/roles/apply/defaults/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/apply/deploy.yaml b/roles/apply/deploy.yaml new file mode 100644 index 0000000..24e6197 --- /dev/null +++ b/roles/apply/deploy.yaml @@ -0,0 +1,11 @@ +--- +- name: Deploy roles + hosts: localhost + become: true + become_user: root + become_method: ansible.builtin.sudo + gather_facts: true + vars_files: + - ../../vars/secrets.yml + roles: + - apply \ No newline at end of file diff --git a/roles/apply/files/.gitkeep b/roles/apply/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/apply/handlers/.gitkeep b/roles/apply/handlers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/apply/handlers/main.yaml b/roles/apply/handlers/main.yaml new file mode 100644 index 0000000..127cc2f --- /dev/null +++ b/roles/apply/handlers/main.yaml @@ -0,0 +1,4 @@ +--- +- name: Log cleanup results + ansible.builtin.debug: + msg: "Removed {{ (old_configs.files | sort(attribute='mtime'))[:-10] | length }} old config files" diff --git a/roles/apply/meta/.gitkeep b/roles/apply/meta/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/apply/tasks/main.yaml b/roles/apply/tasks/main.yaml new file mode 100644 index 0000000..7ea19d9 --- /dev/null +++ b/roles/apply/tasks/main.yaml @@ -0,0 +1,3 @@ +--- +- name: "Подключаем таски" + include_tasks: "role/main.yaml" \ No newline at end of file diff --git a/roles/apply/tasks/role/main.yaml b/roles/apply/tasks/role/main.yaml new file mode 100644 index 0000000..8bbcb96 --- /dev/null +++ b/roles/apply/tasks/role/main.yaml @@ -0,0 +1,130 @@ +--- +- name: Apply new configuration + ansible.builtin.uri: + url: "http://{{ patroni_host }}:8008/config" + method: PATCH + body: "{{ new_config | to_json }}" + body_format: json + status_code: 200 + headers: + Content-Type: "application/json" + register: apply_result + changed_when: apply_result.status == 200 + +- name: Force wait for config to apply # noqa: no-handler + ansible.builtin.wait_for: + timeout: 30 + delay: 5 + when: apply_result is changed + +- name: Get verified cluster status # noqa: no-handler + ansible.builtin.uri: + url: "http://{{ patroni_host }}:8008/cluster" + method: GET + return_content: yes + status_code: 200 + register: verified_cluster_status + delegate_to: localhost + connection: local + when: apply_result is changed + +- name: Display confirmed cluster status + ansible.builtin.debug: + msg: | + === CONFIRMED CLUSTER STATUS === + Leader: {{ (verified_cluster_status.json.members | selectattr('role', 'equalto', 'leader') | map(attribute='name') | first) | default('UNKNOWN') }} + Members: + {% for member in verified_cluster_status.json.members %} + - {{ member.name }} [{{ member.role | upper }}] + State: {{ member.state | default('UNKNOWN') }} + Lag: {{ member.lag | default(0) }}MB + Timeline: {{ member.timeline | default('N/A') }} + Pending restart: {{ member.pending_restart | default(false) | ternary('YES', 'NO') }} + {% endfor %} + Config Applied: {{ apply_result is changed | ternary('YES', 'NO') }} + ================================ + delegate_to: localhost + connection: local + run_once: true + +- name: Refresh cluster status + ansible.builtin.uri: + url: "http://{{ patroni_host }}:8008/cluster" + method: GET + return_content: yes + status_code: 200 + register: refreshed_cluster_status + delegate_to: localhost + run_once: true + when: verified_cluster_status is defined + +- name: Safe check for pending restarts + ansible.builtin.set_fact: + needs_restart: >- + {{ + (refreshed_cluster_status.json.members | + map(attribute='pending_restart', default=false) | + select('equalto', true) | list | count > 0) or + (refreshed_cluster_status.json.members | + map(attribute='tags.pending_restart', default=false) | + select('equalto', true) | list | count > 0) + }} + node_names: >- + {{ + refreshed_cluster_status.json.members | + map(attribute='name') | + list + }} + when: + - refreshed_cluster_status.json is defined + - refreshed_cluster_status.json.members is defined + run_once: true + delegate_to: localhost + +- name: Show restart warning if needed + ansible.builtin.debug: + msg: | + {% if needs_restart %} + ================================== + ВНИМАНИЕ: ТРЕБУЕТСЯ ПЕРЕЗАГРУЗКА + ================================== + + Не, я конечно могу и сам ролью, но вдруг кластер в проде или еще где!!! + Так что лучше выполнить следующую команду на одной из нод кластера: + + patronictl restart {{ node_names | join(' ') }} + + Затронутые ноды: + {% for node in node_names %} + - {{ node }} + {% endfor %} + {% else %} + ================================== + СТАТУС: Перезагрузка не требуется + ================================== + {% endif %} + delegate_to: localhost + run_once: true + +- name: Archive old configurations + block: + - name: Find old config files + ansible.builtin.find: + path: "{{ config_dir }}" + pattern: "*-config.yaml" + age: "10s" + register: old_configs + delegate_to: localhost + connection: local + + - name: Remove excess configs (keep last 10) + ansible.builtin.file: + path: "{{ item.path }}" + state: absent + loop: "{{ (old_configs.files | sort(attribute='mtime'))[:-10] }}" + when: + - old_configs.matched > 10 + - apply_result is changed + delegate_to: localhost + connection: local + notify: Log cleanup results diff --git a/roles/apply/templates/.gitkeep b/roles/apply/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/apply/tests/.gitkeep b/roles/apply/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/deploy.yaml b/roles/deploy.yaml new file mode 100644 index 0000000..a1f20d5 --- /dev/null +++ b/roles/deploy.yaml @@ -0,0 +1,5 @@ +--- +- name: Подготовка ро�к изменению конфнастроек кла�стера + import_playbook: prepare/deploy.yaml +- name: Применение изменений нас�троек кластера + import_playbook: apply/deploy.yaml diff --git a/roles/prepare/defaults/.gitkeep b/roles/prepare/defaults/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/prepare/deploy.yaml b/roles/prepare/deploy.yaml new file mode 100644 index 0000000..1cb5538 --- /dev/null +++ b/roles/prepare/deploy.yaml @@ -0,0 +1,11 @@ +--- +- name: Deploy roles + hosts: localhost + become: true + become_user: root + become_method: ansible.builtin.sudo + gather_facts: true + vars_files: + - ../../vars/secrets.yml + roles: + - prepare \ No newline at end of file diff --git a/roles/prepare/files/.gitkeep b/roles/prepare/files/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/prepare/handlers/.gitkeep b/roles/prepare/handlers/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/prepare/meta/.gitkeep b/roles/prepare/meta/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/prepare/tasks/main.yaml b/roles/prepare/tasks/main.yaml new file mode 100644 index 0000000..86dc755 --- /dev/null +++ b/roles/prepare/tasks/main.yaml @@ -0,0 +1,15 @@ +--- +# - name: "Определяем ОС" +# set_fact: +# os_family: "{{ ansible_facts['os_family'] }}" + +# - name: "Подключаем таски для RedHat совместимых" +# include_tasks: "redhat/main.yaml" +# when: os_family == "RedHat" + +# - name: "Подключаем таски для Debian/Ubuntu совместимых" +# include_tasks: "debian/main.yaml" +# when: os_family == "Debian" + +- name: "Подключаем таски" + include_tasks: "role/main.yaml" \ No newline at end of file diff --git a/roles/prepare/tasks/role/main.yaml b/roles/prepare/tasks/role/main.yaml new file mode 100644 index 0000000..13e539b --- /dev/null +++ b/roles/prepare/tasks/role/main.yaml @@ -0,0 +1,128 @@ +--- +- name: Validate config file exists + ansible.builtin.stat: + path: "{{ config_file }}" + register: config_check + delegate_to: localhost + connection: local + +- name: Fail if config missing + ansible.builtin.fail: + msg: "Configuration file {{ config_file }} not found!" + when: not config_check.stat.exists + +- name: Ensure history directory exists + ansible.builtin.file: + path: "{{ config_dir }}" + state: directory + mode: '0750' + owner: "{{ ansible_user_id | default(omit) }}" + group: "{{ ansible_user_gid | default(omit) }}" + delegate_to: localhost + connection: local + changed_when: false + +- name: Get current Patroni configuration + ansible.builtin.uri: + url: "http://{{ patroni_host }}:8008/config" + method: GET + return_content: yes + status_code: 200 + register: patroni_config + changed_when: false + +- name: Save current config with timestamp + ansible.builtin.copy: + content: | + # Original config from {{ ansible_date_time.iso8601 }} + {{ patroni_config.content | from_json | to_nice_yaml }} + dest: "{{ config_dir }}/{{ ansible_date_time.iso8601 | replace('T', '_') | replace(':', '-') | replace('+', '-UTC') }}-config.yaml" + mode: '0640' + owner: "{{ ansible_user_id | default(omit) }}" + group: "{{ ansible_user_gid | default(omit) }}" + delegate_to: localhost + connection: local + changed_when: false + +- name: Load configuration changes + ansible.builtin.include_vars: + file: "{{ config_file }}" + name: config_changes + delegate_to: localhost + connection: local + +- name: Create merged configuration + ansible.builtin.set_fact: + new_config: "{{ patroni_config.content | from_json | combine(config_changes, recursive=True) }}" + +- name: Validate configuration changes + ansible.builtin.command: + cmd: | + python3 -c ' + import yaml, json, sys; + try: + yaml.safe_load(sys.stdin.read()) + sys.exit(0) + except Exception as e: + print(f"Invalid YAML: {str(e)}") + sys.exit(1)' + stdin: "{{ new_config | to_nice_yaml }}" + register: config_validation + changed_when: false + delegate_to: localhost + connection: local + +- name: Save new config as last.yaml + ansible.builtin.copy: + content: | + # Updated at {{ ansible_date_time.iso8601 }} + {{ new_config | to_nice_yaml }} + dest: "{{ config_dir }}/last.yaml" + mode: '0640' + owner: "{{ ansible_user_id | default(omit) }}" + group: "{{ ansible_user_gid | default(omit) }}" + delegate_to: localhost + connection: local + changed_when: false + +- name: Generate and display colored diff + block: + - name: Create temp files for diff + ansible.builtin.shell: | + cat > /tmp/old_config.yml << 'EOL' + {{ patroni_config.content | from_json | to_nice_yaml }} + EOL + cat > /tmp/new_config.yml << 'EOL' + {{ new_config | to_nice_yaml }} + EOL + delegate_to: localhost + connection: local + changed_when: false + + - name: Execute diff with colors + ansible.builtin.command: > + diff --color=always -u /tmp/old_config.yml /tmp/new_config.yml + register: diff_result + changed_when: diff_result.rc in [0,1] + failed_when: diff_result.rc > 1 + delegate_to: localhost + connection: local + + - name: Cleanup temp files + ansible.builtin.file: + path: "/tmp/{{ item }}" + state: absent + loop: + - old_config.yml + - new_config.yml + delegate_to: localhost + connection: local + changed_when: false + + - name: Display diff line by line + ansible.builtin.debug: + msg: "{{ item }}" + loop: "{{ diff_result.stdout_lines }}" + when: diff_result.stdout_lines | length > 0 + loop_control: + label: "" diff --git a/roles/prepare/templates/.gitkeep b/roles/prepare/templates/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/roles/prepare/tests/.gitkeep b/roles/prepare/tests/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vars/.gitkeep b/vars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/vars/secrets.yml b/vars/secrets.yml new file mode 100644 index 0000000..8bbfaa7 --- /dev/null +++ b/vars/secrets.yml @@ -0,0 +1,10 @@ +$ANSIBLE_VAULT;1.1;AES256 +37376136623761343135636239653137353661303631663536613265366431333339663866643265 +3033653765613632313661393166363238643137346330620a643233623433633963333035646466 +34633366623262643165326331333937623064356131306663623362663663343861383735616365 +3363646132393166310a353965346531616330396666383732656430633630323438326161323965 +64323865636265303331663166393232376138663965613361623361303663353737623238373435 +30316161616234356264643762653036626132613664316137646665323335663232393535353131 +37636331646364313839653438323461353638363936623131626161353936303839393533326162 +31623833313834646233303961656633633933386135396439373463623362316561313138643631 +6663 diff --git a/vault-password.txt b/vault-password.txt new file mode 100644 index 0000000..ad366d9 --- /dev/null +++ b/vault-password.txt @@ -0,0 +1 @@ +password123