--- - name: Развернуть сервер AWatch-rus hosts: aw_server become: true gather_facts: true vars: aw_release_root: "/opt/activitywatch/releases" aw_release_dir: "{{ aw_release_root }}/{{ aw_server_version }}" aw_archive_path: "/tmp/activitywatch-{{ aw_server_version }}.zip" aw_bootstrap_dir: "/tmp/aw-rus-bootstrap" aw_release_install_dir: "{{ aw_release_root }}/aw-server-rust-{{ aw_server_version }}" aw_ru_patch_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-ru-patch.js') | hash('sha1') | truncate(12, true, '') }}" aw_sw_cleanup_cache_bust: "{{ lookup('file', aw_repo_root + '/aw-server/aw-sw-cleanup.js') | hash('sha1') | truncate(12, true, '') }}" aw_worktime_classes: "{{ lookup('file', aw_repo_root + '/aw-server/settings/classes-worktime.json') | from_json }}" aw_default_views: "{{ lookup('file', aw_repo_root + '/aw-server/settings/views-default.json') | from_json }}" tasks: - name: Установить базовые пакеты ansible.builtin.apt: name: - curl - python3-venv - python3-pip - rsync - tesseract-ocr - tesseract-ocr-rus - unzip state: present update_cache: true - name: Создать системную группу сервиса ansible.builtin.group: name: "{{ aw_server_group }}" system: true state: present - name: Создать системную учётную запись сервиса ansible.builtin.user: name: "{{ aw_server_user }}" group: "{{ aw_server_group }}" home: "{{ aw_server_data_dir }}" shell: /usr/sbin/nologin system: true create_home: false - name: Создать обязательные каталоги ansible.builtin.file: path: "{{ item }}" state: directory mode: "0755" loop: - "{{ aw_release_root }}" - "{{ aw_release_dir }}" - "{{ aw_release_install_dir }}" - /opt/activitywatch - /opt/activitywatch/bin - "{{ aw_server_webui_dir }}" - "{{ aw_server_webui_dir }}/js" - "{{ aw_server_data_dir }}" - "{{ aw_server_db_path | dirname }}" - "{{ aw_server_data_dir }}/.config" - "{{ aw_server_data_dir }}/.config/activitywatch" - "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust" - "{{ aw_server_data_dir }}/backups" - "{{ aw_server_log_dir }}" - /etc/activitywatch - "{{ aw_bootstrap_dir }}" - name: Настроить каталоги ActivityWatch с владельцем сервиса ansible.builtin.file: path: "{{ item }}" state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" loop: - /opt/activitywatch - /opt/activitywatch/bin - "{{ aw_release_root }}" - "{{ aw_release_dir }}" - "{{ aw_release_install_dir }}" - "{{ aw_server_webui_dir }}" - "{{ aw_server_webui_dir }}/js" - "{{ aw_server_data_dir }}" - "{{ aw_server_db_path | dirname }}" - "{{ aw_server_data_dir }}/.config" - "{{ aw_server_data_dir }}/.config/activitywatch" - "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust" - "{{ aw_server_data_dir }}/backups" - "{{ aw_server_log_dir }}" - name: (Check mode) Пропустить установку релиза ActivityWatch ansible.builtin.debug: msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped." when: ansible_check_mode - name: Установить релиз ActivityWatch (download/unarchive/install) when: not ansible_check_mode block: - name: Скачать архив релиза ActivityWatch ansible.builtin.get_url: url: "{{ aw_server_download_url }}" dest: "{{ aw_archive_path }}" mode: "0644" - name: Распаковать релиз ActivityWatch ansible.builtin.unarchive: src: "{{ aw_archive_path }}" dest: "{{ aw_release_dir }}" remote_src: true extra_opts: ["-o"] - name: Найти распакованный каталог ActivityWatch ansible.builtin.find: paths: "{{ aw_release_dir }}" recurse: true file_type: directory patterns: "activitywatch*" register: aw_release_find - name: Найти бинарный файл AW server ansible.builtin.find: paths: "{{ aw_release_dir }}" recurse: true file_type: file patterns: - aw-server-rust - aw-server register: aw_server_binary_find - name: Найти index.html WebUI ansible.builtin.find: paths: "{{ aw_release_dir }}" recurse: true file_type: file patterns: - index.html register: aw_webui_index_find - name: Сохранить пути распакованного релиза (binary + webui index) ansible.builtin.set_fact: aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}" aw_server_binary_path: >- {{ ( ( (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list) | select('match', '.*/aw-server-rust$') | list | first ) | default( ( (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) ), true ) ) | default('') }} aw_webui_index_path: >- {{ ( ( (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list) | select('search', '/static/index\\.html$') | list | first ) | default( ( (aw_webui_index_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) ), true ) ) | default('') }} - name: Сохранить каталог WebUI (dirname index.html) ansible.builtin.set_fact: aw_webui_source_path: "{{ aw_webui_index_path | dirname }}" - name: Проверить, что компоненты релиза найдены ansible.builtin.assert: that: - aw_release_extracted is defined - aw_release_extracted | length > 0 - aw_server_binary_path is defined - aw_server_binary_path | length > 0 - aw_webui_source_path is defined - aw_webui_source_path | length > 0 fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch." - name: Создать каталог установленного релиза ansible.builtin.file: path: "{{ aw_release_install_dir }}" state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" - name: Установить бинарный файл AW server ansible.builtin.copy: remote_src: true src: "{{ aw_server_binary_path }}" dest: "{{ aw_release_install_dir }}/aw-server-rust" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" - name: Создать ссылку на активный бинарный файл AW server ansible.builtin.file: src: "{{ aw_release_install_dir }}/aw-server-rust" dest: /opt/activitywatch/bin/aw-server-rust owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" state: link force: true - name: Синхронизировать WebUI в RU каталог ansible.builtin.command: cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/" - name: Настроить владельца файлов /opt/activitywatch ansible.builtin.file: path: /opt/activitywatch state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" recurse: true - name: Установить systemd service из шаблона репозитория ansible.builtin.copy: dest: /etc/systemd/system/activitywatch-server.service mode: "0644" content: >- {{ lookup('file', aw_repo_root + '/aw-server/activitywatch-server.service') | replace('__AW_SERVER_USER__', aw_server_user) | replace('__AW_SERVER_GROUP__', aw_server_group) | replace('__AW_SERVER_DATA_DIR__', aw_server_data_dir) }} notify: - Перезагрузить systemd - Перезапустить activitywatch - name: (Check mode) Пропустить WebUI patch и запуск сервиса ansible.builtin.debug: msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped." when: ansible_check_mode - name: Применить WebUI RU patch и запустить сервис when: not ansible_check_mode block: - name: Скопировать RU patch файлы WebUI из репозитория ansible.builtin.copy: src: "{{ item.src }}" dest: "{{ item.dest }}" mode: "{{ item.mode }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" loop: - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "{{ aw_server_webui_dir }}/js/ru-patch-v5.js", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "{{ aw_server_webui_dir }}/js/sw-cleanup.js", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "{{ aw_server_webui_dir }}/js/aw-worktime-panel.js", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "{{ aw_server_webui_dir }}/js/aw-host-groups.json", mode: "0644" } - name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh ansible.builtin.file: path: /root/bootstrap state: directory mode: "0755" - name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков) ansible.builtin.copy: src: "{{ item.src }}" dest: "{{ item.dest }}" mode: "{{ item.mode }}" loop: - { src: "{{ aw_repo_root }}/aw-server/aw-ru-patch.js", dest: "/root/bootstrap/aw-ru-patch.js", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-sw-cleanup.js", dest: "/root/bootstrap/aw-sw-cleanup.js", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-worktime-panel.js", dest: "/root/bootstrap/aw-worktime-panel.js", mode: "0644" } - { src: "{{ aw_repo_root }}/aw-server/aw-host-groups.json", dest: "/root/bootstrap/aw-host-groups.json", mode: "0644" } - name: Скопировать apply_webui_ru_patch.sh скрипт ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh" dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh mode: "0755" - name: Записать /etc/activitywatch/aw-server.env перед хотфиксами ansible.builtin.copy: dest: /etc/activitywatch/aw-server.env mode: "0640" owner: root group: root content: | AW_SERVER_BIND_HOST={{ aw_server_bind_host }} AW_SERVER_PORT={{ aw_server_port }} AW_SERVER_DATA_DIR={{ aw_server_data_dir }} AW_SERVER_DB_PATH={{ aw_server_db_path }} AW_SERVER_LOG_DIR={{ aw_server_log_dir }} AW_SERVER_WEBUI_DIR={{ aw_server_webui_dir }} AW_SERVER_USER={{ aw_server_user }} AW_SERVER_GROUP={{ aw_server_group }} AW_WORKTIME_REPORT_BASE={{ aw_worktime_report_base }} AW_WORKTIME_TZ={{ aw_worktime_timezone }} AW_DLP_IOC_DIR={{ aw_dlp_ioc_workdir }}/output AW_DLP_POLICY_ENGINE_BIND_HOST={{ aw_dlp_policy_engine_bind_host }} AW_DLP_POLICY_ENGINE_PORT={{ aw_dlp_policy_engine_port }} AW_DLP_POLICY_ENGINE_DB_PATH={{ aw_dlp_policy_engine_db_path }} AW_DLP_CASE_BIND_HOST={{ aw_dlp_case_bind_host }} AW_DLP_CASE_PORT={{ aw_dlp_case_port }} AW_DLP_CASE_DB_PATH={{ aw_dlp_case_db_path }} XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config - name: Создать каталог DLP policy engine ansible.builtin.file: path: /opt/activitywatch/dlp-policy-engine state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" when: aw_dlp_policy_engine_enabled | default(false) | bool - name: Скопировать файлы DLP policy engine ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-policy-engine/{{ item }}" dest: "/opt/activitywatch/dlp-policy-engine/{{ item }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" loop: - policy_service.py - policy_schema.py - policy_storage.py - policy_distributor.py - requirements.txt when: aw_dlp_policy_engine_enabled | default(false) | bool - name: Создать virtualenv DLP policy engine ansible.builtin.command: cmd: python3 -m venv /opt/activitywatch/dlp-policy-engine/.venv args: creates: /opt/activitywatch/dlp-policy-engine/.venv/bin/python when: aw_dlp_policy_engine_enabled | default(false) | bool - name: Установить зависимости DLP policy engine ansible.builtin.pip: requirements: /opt/activitywatch/dlp-policy-engine/requirements.txt virtualenv: /opt/activitywatch/dlp-policy-engine/.venv when: aw_dlp_policy_engine_enabled | default(false) | bool - name: Установить systemd unit DLP policy engine ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-policy-engine/dlp-policy-engine.service" dest: /etc/systemd/system/aw-dlp-policy-engine.service owner: root group: root mode: "0644" when: aw_dlp_policy_engine_enabled | default(false) | bool - name: Создать каталог DLP content analysis ansible.builtin.file: path: /opt/activitywatch/dlp-content-analysis state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" when: aw_dlp_content_analysis_enabled | default(true) | bool - name: Скопировать файлы DLP content analysis ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-content-analysis/" dest: /opt/activitywatch/dlp-content-analysis/ owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" when: aw_dlp_content_analysis_enabled | default(true) | bool - name: Создать virtualenv DLP content analysis ansible.builtin.command: cmd: python3 -m venv /opt/activitywatch/dlp-content-analysis/.venv args: creates: /opt/activitywatch/dlp-content-analysis/.venv/bin/python when: aw_dlp_content_analysis_enabled | default(true) | bool - name: Установить зависимости DLP content analysis ansible.builtin.pip: requirements: /opt/activitywatch/dlp-content-analysis/requirements.txt virtualenv: /opt/activitywatch/dlp-content-analysis/.venv when: aw_dlp_content_analysis_enabled | default(true) | bool - name: Создать каталог DLP integrations ansible.builtin.file: path: /opt/activitywatch/dlp-integrations state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" when: aw_dlp_integrations_enabled | default(true) | bool - name: Скопировать файлы DLP integrations ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-integrations/{{ item }}" dest: "/opt/activitywatch/dlp-integrations/{{ item }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" loop: - cef_exporter.py - webhook_sender.py - cef-config.yaml - webhook-config.yaml - requirements.txt when: aw_dlp_integrations_enabled | default(true) | bool - name: Выставить executable для python scripts DLP integrations ansible.builtin.file: path: "/opt/activitywatch/dlp-integrations/{{ item }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" loop: - cef_exporter.py - webhook_sender.py when: aw_dlp_integrations_enabled | default(true) | bool - name: Создать virtualenv DLP integrations ansible.builtin.command: cmd: python3 -m venv /opt/activitywatch/dlp-integrations/.venv args: creates: /opt/activitywatch/dlp-integrations/.venv/bin/python when: aw_dlp_integrations_enabled | default(true) | bool - name: Установить зависимости DLP integrations ansible.builtin.pip: requirements: /opt/activitywatch/dlp-integrations/requirements.txt virtualenv: /opt/activitywatch/dlp-integrations/.venv when: aw_dlp_integrations_enabled | default(true) | bool - name: Создать state каталог DLP integrations ansible.builtin.file: path: /var/lib/activitywatch/dlp-integrations state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" when: aw_dlp_integrations_enabled | default(true) | bool - name: Установить systemd unit CEF exporter ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-integrations/cef-exporter.service" dest: /etc/systemd/system/aw-dlp-cef-exporter.service owner: root group: root mode: "0644" when: aw_dlp_integrations_enabled | default(true) | bool - name: Установить systemd timer CEF exporter ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-integrations/cef-exporter.timer" dest: /etc/systemd/system/aw-dlp-cef-exporter.timer owner: root group: root mode: "0644" when: aw_dlp_integrations_enabled | default(true) | bool - name: Установить systemd unit webhook sender ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-integrations/webhook-sender.service" dest: /etc/systemd/system/aw-dlp-webhook-sender.service owner: root group: root mode: "0644" when: aw_dlp_integrations_enabled | default(true) | bool - name: Установить systemd timer webhook sender ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-integrations/webhook-sender.timer" dest: /etc/systemd/system/aw-dlp-webhook-sender.timer owner: root group: root mode: "0644" when: aw_dlp_integrations_enabled | default(true) | bool - name: Создать каталог DLP case management ansible.builtin.file: path: /opt/activitywatch/dlp-case-management state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" when: aw_dlp_case_management_enabled | default(true) | bool - name: Скопировать файлы DLP case management ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-case-management/{{ item }}" dest: "/opt/activitywatch/dlp-case-management/{{ item }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" loop: - case_service.py - case_schema.py - case_storage.py - requirements.txt when: aw_dlp_case_management_enabled | default(true) | bool - name: Выставить executable для case_service ansible.builtin.file: path: /opt/activitywatch/dlp-case-management/case_service.py owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" when: aw_dlp_case_management_enabled | default(true) | bool - name: Создать virtualenv DLP case management ansible.builtin.command: cmd: python3 -m venv /opt/activitywatch/dlp-case-management/.venv args: creates: /opt/activitywatch/dlp-case-management/.venv/bin/python when: aw_dlp_case_management_enabled | default(true) | bool - name: Установить зависимости DLP case management ansible.builtin.pip: requirements: /opt/activitywatch/dlp-case-management/requirements.txt virtualenv: /opt/activitywatch/dlp-case-management/.venv when: aw_dlp_case_management_enabled | default(true) | bool - name: Установить systemd unit DLP case management ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/dlp-case-management/case-service.service" dest: /etc/systemd/system/aw-dlp-case-management.service owner: root group: root mode: "0644" when: aw_dlp_case_management_enabled | default(true) | bool - name: Установить скрипт AW worktime API ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py" dest: /usr/local/bin/aw-worktime-api.py owner: root group: root mode: "0755" - name: Установить systemd unit AW worktime API ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.service" dest: /etc/systemd/system/aw-worktime-api.service owner: root group: root mode: "0644" - name: Установить скрипт AW worktime UI bridge ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.py" dest: /usr/local/bin/aw-worktime-ui-bridge.py owner: root group: root mode: "0755" - name: Установить скрипт AW worktime autoheal ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-autoheal.sh" dest: /usr/local/bin/aw-worktime-autoheal.sh owner: root group: root mode: "0755" - name: Установить скрипт aw-health-check ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/health-check.sh" dest: /usr/local/bin/aw-health-check owner: root group: root mode: "0755" - name: Установить systemd unit AW worktime UI bridge ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.service" dest: /etc/systemd/system/aw-worktime-ui-bridge.service owner: root group: root mode: "0644" - name: Установить systemd timer AW worktime UI bridge ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-ui-bridge.timer" dest: /etc/systemd/system/aw-worktime-ui-bridge.timer owner: root group: root mode: "0644" - name: Установить systemd unit AW worktime autoheal ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-autoheal.service" dest: /etc/systemd/system/aw-worktime-autoheal.service owner: root group: root mode: "0644" - name: Установить systemd timer AW worktime autoheal ansible.builtin.copy: src: "{{ aw_repo_root }}/aw-server/aw-worktime-autoheal.timer" dest: /etc/systemd/system/aw-worktime-autoheal.timer owner: root group: root mode: "0644" - name: Перезагрузить systemd после установки AW worktime API ansible.builtin.systemd: daemon_reload: true - name: Включить и перезапустить DLP policy engine ansible.builtin.systemd: name: aw-dlp-policy-engine.service enabled: true state: restarted when: aw_dlp_policy_engine_enabled | default(false) | bool - name: Включить и перезапустить timer CEF exporter ansible.builtin.systemd: name: aw-dlp-cef-exporter.timer enabled: true state: restarted when: aw_dlp_integrations_enabled | default(true) | bool - name: Включить и перезапустить timer webhook sender ansible.builtin.systemd: name: aw-dlp-webhook-sender.timer enabled: true state: restarted when: aw_dlp_integrations_enabled | default(true) | bool - name: Включить и перезапустить DLP case management ansible.builtin.systemd: name: aw-dlp-case-management.service enabled: true state: restarted when: aw_dlp_case_management_enabled | default(true) | bool - name: Включить и перезапустить AW worktime API ansible.builtin.systemd: name: aw-worktime-api.service enabled: true state: restarted - name: Отключить legacy timer aw-worktime-afk-bridge (если есть) ansible.builtin.systemd: name: aw-worktime-afk-bridge.timer enabled: false state: stopped failed_when: false - name: Включить и перезапустить AW worktime UI bridge timer ansible.builtin.systemd: name: aw-worktime-ui-bridge.timer enabled: true state: restarted - name: Выполнить разовый прогон AW worktime UI bridge ansible.builtin.systemd: name: aw-worktime-ui-bridge.service state: started failed_when: false - name: Включить и перезапустить AW worktime autoheal timer ansible.builtin.systemd: name: aw-worktime-autoheal.timer enabled: true state: restarted - name: Выполнить разовый прогон AW worktime autoheal ansible.builtin.systemd: name: aw-worktime-autoheal.service state: started failed_when: false - name: Применить хотфиксы compiled JS чанков (Trends, Timespiral, Category helper) ansible.builtin.command: cmd: "/opt/activitywatch/aw-server/apply_webui_ru_patch.sh" register: apply_ru_patch_result failed_when: false - name: Вывести результат применения хотфиксов ansible.builtin.debug: msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}" - name: Проверить наличие index.html после копирования ansible.builtin.stat: path: "{{ aw_server_webui_dir }}/index.html" register: aw_webui_ru_index - name: Проверить, что index.html доступен для RU patch ansible.builtin.assert: that: - aw_webui_ru_index.stat.exists fail_msg: "Не найден index.html WebUI для применения RU patch." - name: Удалить старые теги RU patch из index.html ansible.builtin.replace: path: "{{ aw_server_webui_dir }}/index.html" regexp: ']+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*>' replace: '' - name: Добавить cleanup script RU patch в index.html ansible.builtin.replace: path: "{{ aw_server_webui_dir }}/index.html" regexp: '' replace: '' - name: Добавить загрузчик RU patch перед закрытием body ansible.builtin.replace: path: "{{ aw_server_webui_dir }}/index.html" regexp: '' replace: '' - name: Скопировать merge script AW DB на сервер ansible.builtin.copy: src: "{{ aw_repo_root }}/scripts/merge_aw_server_dbs.py" dest: /usr/local/bin/merge_aw_server_dbs.py owner: root group: root mode: "0755" - name: Проверить наличие legacy root DB ansible.builtin.stat: path: /root/.local/share/activitywatch/aw-server-rust/sqlite.db register: aw_legacy_root_db - name: Проверить наличие target DB ansible.builtin.stat: path: "{{ aw_server_db_path }}" register: aw_target_db - name: Остановить сервис перед merge server DB ansible.builtin.systemd: name: activitywatch-server.service state: stopped when: aw_legacy_root_db.stat.exists | default(false) - name: Создать backup каталоги server DB ansible.builtin.file: path: "{{ aw_server_data_dir }}/backups/db" state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" when: aw_legacy_root_db.stat.exists | default(false) - name: Backup target DB перед merge ansible.builtin.copy: remote_src: true src: "{{ aw_server_db_path }}" dest: "{{ aw_server_data_dir }}/backups/db/target-before-merge-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" when: - aw_legacy_root_db.stat.exists | default(false) - aw_target_db.stat.exists | default(false) - name: Backup legacy root DB перед merge ansible.builtin.copy: remote_src: true src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db dest: "{{ aw_server_data_dir }}/backups/db/legacy-root-{{ ansible_date_time.iso8601_basic_short }}.sqlite.db" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" when: aw_legacy_root_db.stat.exists | default(false) - name: Merge legacy root DB в target DB ansible.builtin.command: argv: - python3 - /usr/local/bin/merge_aw_server_dbs.py - --base - /root/.local/share/activitywatch/aw-server-rust/sqlite.db - --overlay - "{{ aw_server_db_path }}" - --output - "{{ aw_server_db_path }}.merged" register: aw_merge_result failed_when: false when: - aw_legacy_root_db.stat.exists | default(false) - aw_target_db.stat.exists | default(false) - name: Показать результат merge legacy root DB ansible.builtin.debug: msg: "{{ aw_merge_result.stdout | default(aw_merge_result.stderr | default('merge not executed')) }}" when: - aw_legacy_root_db.stat.exists | default(false) - aw_target_db.stat.exists | default(false) - name: Install merged DB as active target DB ansible.builtin.copy: remote_src: true src: "{{ aw_server_db_path }}.merged" dest: "{{ aw_server_db_path }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" when: - aw_legacy_root_db.stat.exists | default(false) - aw_target_db.stat.exists | default(false) - aw_merge_result is defined - (aw_merge_result.rc | default(1) | int) == 0 - name: Скопировать legacy root DB в target DB если target ещё не существует ansible.builtin.copy: remote_src: true src: /root/.local/share/activitywatch/aw-server-rust/sqlite.db dest: "{{ aw_server_db_path }}" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" when: - aw_legacy_root_db.stat.exists | default(false) - not (aw_target_db.stat.exists | default(false)) - name: Записать aw-server-rust config.toml с разрешёнными CORS origin ansible.builtin.copy: dest: "{{ aw_server_data_dir }}/.config/activitywatch/aw-server-rust/config.toml" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" content: | cors = [ {% for origin in aw_server_cors_origins | default([]) %} "{{ origin }}"{% if not loop.last %},{% endif %} {% endfor %} ] - name: Включить и запустить сервис ansible.builtin.systemd: name: activitywatch-server.service enabled: true state: restarted daemon_reload: true - name: Дождаться ответа API ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info" method: GET status_code: 200 register: aw_api retries: 10 delay: 3 until: aw_api.status == 200 - name: Считать текущие server-side settings ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/" method: GET status_code: 200 register: aw_settings_current when: aw_apply_worktime_settings | default(false) | bool - name: Считать текущие server-side views ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views" method: GET status_code: 200 register: aw_views_current when: aw_apply_worktime_settings | default(false) | bool - name: Считать текущие server-side classes ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes" method: GET status_code: 200 register: aw_classes_current when: aw_apply_worktime_settings | default(false) | bool - name: Создать backup текущих server-side settings/views/classes ansible.builtin.copy: dest: "{{ aw_server_data_dir }}/backups/{{ item.name }}-{{ ansible_date_time.iso8601_basic_short }}.json" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0644" content: "{{ item.payload | to_nice_json }}" loop: - name: settings payload: "{{ aw_settings_current.json | default({}) }}" - name: views payload: "{{ aw_views_current.json | default(none) }}" - name: classes payload: "{{ aw_classes_current.json | default(none) }}" when: aw_apply_worktime_settings | default(false) | bool - name: Настроить DLP Aggregator (Phase 2) block: - name: Создать каталог для скриптов ansible.builtin.file: path: "/opt/activitywatch/scripts" state: directory owner: root group: root mode: "0755" - name: Скопировать агрегатор событий DLP ansible.builtin.copy: src: "{{ aw_repo_root }}/scripts/aggregate_dlp_events.py" dest: "/opt/activitywatch/scripts/aggregate_dlp_events.py" owner: root group: root mode: "0755" - name: Установить systemd unit для агрегатора ansible.builtin.copy: dest: /etc/systemd/system/activitywatch-dlp-aggregator.service content: | [Unit] Description=ActivityWatch DLP Event Aggregator After=activitywatch-server.service [Service] Type=oneshot User={{ aw_server_user }} WorkingDirectory={{ aw_server_data_dir }} ExecStart=/usr/bin/python3 /opt/activitywatch/scripts/aggregate_dlp_events.py \ --aw-url http://127.0.0.1:{{ aw_server_port }}/api/0 \ --sqlite-path {{ aw_server_data_dir }}/dlp_warehouse.sqlite \ --state-path {{ aw_server_data_dir }}/dlp-aggregator-state.json [Install] WantedBy=multi-user.target - name: Установить systemd timer для агрегатора ansible.builtin.copy: dest: /etc/systemd/system/activitywatch-dlp-aggregator.timer content: | [Unit] Description=Run ActivityWatch DLP Aggregator every 5 minutes [Timer] OnBootSec=1min OnUnitActiveSec=5min AccuracySec=1s [Install] WantedBy=timers.target - name: Включить и запустить таймер агрегатора ansible.builtin.systemd: name: activitywatch-dlp-aggregator.timer enabled: true state: started daemon_reload: true - name: Настроить IOC enrichment из Hayabusa Sigma when: aw_dlp_ioc_enabled | default(false) | bool block: - name: Установить зависимости IOC refresh ansible.builtin.apt: name: - python3-yaml state: present update_cache: true - name: Создать каталог IOC enrichment ansible.builtin.file: path: "{{ aw_dlp_ioc_workdir }}/{{ item }}" state: directory owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" mode: "0755" loop: - "" - output - tmp - name: Установить экстрактор IOC из Sigma ansible.builtin.copy: src: "{{ aw_repo_root }}/scripts/extract_ioc_from_sigma.py" dest: /usr/local/bin/aw-extract-ioc-from-sigma.py owner: root group: root mode: "0755" - name: Установить wrapper refresh IOC ansible.builtin.copy: dest: /usr/local/bin/aw-dlp-ioc-refresh.sh owner: root group: root mode: "0755" content: | #!/usr/bin/env bash set -euo pipefail WORKDIR="{{ aw_dlp_ioc_workdir }}" OUTDIR="${WORKDIR}/output" TMPDIR="${WORKDIR}/tmp" RULES_DIR="${TMPDIR}/hayabusa-rules" ZIP_PATH="${TMPDIR}/hayabusa-rules.zip" mkdir -p "${OUTDIR}" "${TMPDIR}" rm -rf "${RULES_DIR}" curl -fsSL -o "${ZIP_PATH}" "{{ aw_dlp_ioc_rules_zip_url }}" unzip -q -o "${ZIP_PATH}" -d "${TMPDIR}" EXTRACTED_DIR=$(find "${TMPDIR}" -maxdepth 1 -type d -name 'hayabusa-rules-*' | head -n1) if [ -z "${EXTRACTED_DIR}" ]; then echo "ERROR: hayabusa-rules archive extraction failed" >&2 exit 1 fi mv "${EXTRACTED_DIR}" "${RULES_DIR}" /usr/bin/python3 /usr/local/bin/aw-extract-ioc-from-sigma.py \ --rules-root "${RULES_DIR}" \ --out-dir "${OUTDIR}" ln -sfn "${OUTDIR}" "${WORKDIR}/latest" - name: Установить systemd unit IOC refresh ansible.builtin.copy: dest: /etc/systemd/system/aw-dlp-ioc-refresh.service owner: root group: root mode: "0644" content: | [Unit] Description=Refresh DLP IOC blacklist from Hayabusa Sigma rules After=network-online.target Wants=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/aw-dlp-ioc-refresh.sh - name: Установить systemd timer IOC refresh ansible.builtin.copy: dest: /etc/systemd/system/aw-dlp-ioc-refresh.timer owner: root group: root mode: "0644" content: | [Unit] Description=Run DLP IOC refresh from Hayabusa rules [Timer] OnBootSec={{ aw_dlp_ioc_refresh_on_boot_sec }} OnUnitActiveSec={{ aw_dlp_ioc_refresh_interval }} Persistent=true Unit=aw-dlp-ioc-refresh.service [Install] WantedBy=timers.target - name: Включить и запустить IOC refresh timer ansible.builtin.systemd: name: aw-dlp-ioc-refresh.timer enabled: true state: started daemon_reload: true - name: Выполнить принудительный refresh IOC ansible.builtin.systemd: name: aw-dlp-ioc-refresh.service state: started - name: Проверить наличие IOC артефактов после refresh ansible.builtin.stat: path: "{{ aw_dlp_ioc_workdir }}/output/{{ item }}" register: aw_dlp_ioc_artifacts loop: - ioc_blacklist.json - ioc_blacklist.csv - ioc_blacklist.sql - name: Assert по IOC артефактам ansible.builtin.assert: that: - item.stat.exists - (item.stat.size | int) > 100 fail_msg: "Не сгенерирован IOC артефакт: {{ item.stat.path | default('unknown') }}" loop: "{{ aw_dlp_ioc_artifacts.results }}" - name: Применить базовые worktime settings (classes) ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/classes" method: POST body: "{{ aw_worktime_classes }}" body_format: json status_code: [200, 201] when: aw_apply_worktime_settings | default(false) | bool - name: Применить базовые views для DLP и worktime ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/views" method: POST body: "{{ aw_default_views }}" body_format: json status_code: [200, 201] when: aw_apply_worktime_settings | default(false) | bool - name: Вычислить worktime durationDefault из aw_worktime_from/to ansible.builtin.set_fact: aw_worktime_duration_default_derived: >- {{ ( ( (((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) - (((aw_worktime_from | default('08:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('08:00')).split(':')[1] | int)) ) * 60 ) }} when: aw_apply_worktime_settings | default(false) | bool - name: Нормализовать durationDefault для ночных смен ansible.builtin.set_fact: aw_worktime_duration_default_effective: >- {{ (aw_worktime_duration_default_derived | int) if (aw_worktime_duration_default_derived | int) > 0 else ((aw_worktime_duration_default_derived | int) + 86400) }} when: aw_apply_worktime_settings | default(false) | bool - name: Проверить корректность durationDefault ansible.builtin.assert: that: - aw_worktime_duration_default_effective | int > 0 - aw_worktime_duration_default_effective | int <= 86400 fail_msg: "Некорректный интервал рабочего времени: {{ aw_worktime_from }}..{{ aw_worktime_to }}" when: aw_apply_worktime_settings | default(false) | bool - name: Применить базовый период worktime (startOfDay) ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay" method: POST body: "\"{{ aw_worktime_start_of_day }}\"" headers: Content-Type: application/json status_code: [200, 201] when: aw_apply_worktime_settings | default(false) | bool - name: Применить базовый период worktime (durationDefault seconds) ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault" method: POST body: "{{ aw_worktime_duration_default_effective | string }}" headers: Content-Type: application/json status_code: [200, 201] when: aw_apply_worktime_settings | default(false) | bool - name: Применить always_active_pattern для fallback без AFK ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/always_active_pattern" method: POST body: "\"{{ aw_server_always_active_pattern }}\"" headers: Content-Type: application/json status_code: [200, 201] when: - aw_apply_worktime_settings | default(false) | bool - (aw_server_always_active_pattern | default('') | string | length) > 0 - name: Применить landingpage профиля ansible.builtin.uri: url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/landingpage" method: POST body: "\"{{ aw_server_landingpage }}\"" headers: Content-Type: application/json status_code: [200, 201] when: - aw_apply_worktime_settings | default(false) | bool - (aw_server_landingpage | default('') | string | length) > 0 - name: Обновить IOC blacklist из hayabusa (если включено) when: - aw_hayabusa_ioc_refresh_enabled | default(false) | bool - aw_hayabusa_rules_root is defined - aw_hayabusa_ioc_output_dir is defined block: - name: Создать выходную директорию для IOC ansible.builtin.file: path: "{{ aw_hayabusa_ioc_output_dir }}" state: directory mode: "0755" owner: "{{ aw_server_user }}" group: "{{ aw_server_group }}" - name: Обновить IOC blacklist из hayabusa правил ansible.builtin.shell: cmd: | set -euo pipefail {{ aw_repo_root }}/scripts/build_dlp_ioc_from_hayabusa.sh \ "{{ aw_hayabusa_rules_root }}" \ "{{ aw_hayabusa_ioc_output_dir }}" args: chdir: "{{ aw_repo_root }}" register: aw_hayabusa_ioc_refresh_result changed_when: "'IOC artifacts generated' in aw_hayabusa_ioc_refresh_result.stdout" - name: Показать результат обновления IOC ansible.builtin.debug: msg: "{{ aw_hayabusa_ioc_refresh_result.stdout }}" when: aw_hayabusa_ioc_refresh_result.stdout is defined - name: Post-deploy health gate (aw-health-check) when: - not ansible_check_mode - aw_server_post_deploy_health_check_enabled | default(true) | bool block: - name: Запустить /usr/local/bin/aw-health-check ansible.builtin.command: cmd: /usr/local/bin/aw-health-check register: aw_post_deploy_health changed_when: false - name: Показать результат aw-health-check ansible.builtin.debug: msg: "{{ aw_post_deploy_health.stdout }}" handlers: - name: Перезагрузить systemd ansible.builtin.systemd: daemon_reload: true - name: Перезапустить activitywatch ansible.builtin.systemd: name: activitywatch-server.service state: restarted