chore(install-kit): sync package payload with current repo baseline
This commit is contained in:
@@ -31,6 +31,15 @@ cd ansible
|
|||||||
ansible-playbook -i inventory.ini deploy_aw_server.yml
|
ansible-playbook -i inventory.ini deploy_aw_server.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Секреты (пароли) безопасно
|
||||||
|
|
||||||
|
Рекомендуемый способ не хранить пароли в репозитории — перед запуском экспортировать их в переменные окружения:
|
||||||
|
|
||||||
|
- Linux `aw_server` (SSH пароль root): `AW_SSH_PASSWORD`
|
||||||
|
- Windows `aw_windows` (WinRM пароль): `AW_WINRM_PASSWORD`
|
||||||
|
|
||||||
|
В `group_vars/aw_server.yml` и `group_vars/windows.yml` они читаются через `lookup('env', ...)`.
|
||||||
|
|
||||||
## Полный установочный playbook (всё за один запуск)
|
## Полный установочный playbook (всё за один запуск)
|
||||||
|
|
||||||
Если нужно прогнать полный цикл одной командой:
|
Если нужно прогнать полный цикл одной командой:
|
||||||
@@ -149,3 +158,13 @@ Playbook:
|
|||||||
- Для полного сценария CT создаётся автоматически через `pct create`.
|
- Для полного сценария CT создаётся автоматически через `pct create`.
|
||||||
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
|
- На Windows/RDP host развёрнуты AFK/window watchers, browser domain collector, DLP endpoint collector и worktime session collector.
|
||||||
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
|
- Проверочный JSON-отчёт Windows playbook должен иметь `overallOk=true`.
|
||||||
|
|
||||||
|
## Prod rollout одной командой
|
||||||
|
|
||||||
|
Для ручного запуска с dry-run и логированием используйте:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash scripts/prod_rollout.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Скрипт попросит `AW_SSH_PASSWORD` и `AW_WINRM_PASSWORD` интерактивно (ввод скрыт) и сложит логи в `.rollout-logs/`.
|
||||||
|
|||||||
@@ -54,6 +54,11 @@
|
|||||||
- "{{ aw_server_webui_dir }}"
|
- "{{ aw_server_webui_dir }}"
|
||||||
- "{{ aw_server_webui_dir }}/js"
|
- "{{ aw_server_webui_dir }}/js"
|
||||||
- "{{ aw_server_data_dir }}"
|
- "{{ 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 }}"
|
- "{{ aw_server_log_dir }}"
|
||||||
- /etc/activitywatch
|
- /etc/activitywatch
|
||||||
- "{{ aw_bootstrap_dir }}"
|
- "{{ aw_bootstrap_dir }}"
|
||||||
@@ -74,100 +79,147 @@
|
|||||||
- "{{ aw_server_webui_dir }}"
|
- "{{ aw_server_webui_dir }}"
|
||||||
- "{{ aw_server_webui_dir }}/js"
|
- "{{ aw_server_webui_dir }}/js"
|
||||||
- "{{ aw_server_data_dir }}"
|
- "{{ 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 }}"
|
- "{{ aw_server_log_dir }}"
|
||||||
|
|
||||||
- name: Скачать архив релиза ActivityWatch
|
- name: (Check mode) Пропустить установку релиза ActivityWatch
|
||||||
ansible.builtin.get_url:
|
ansible.builtin.debug:
|
||||||
url: "{{ aw_server_download_url }}"
|
msg: "ansible_check_mode=true: download/unarchive/install of ActivityWatch release is skipped."
|
||||||
dest: "{{ aw_archive_path }}"
|
when: ansible_check_mode
|
||||||
mode: "0644"
|
|
||||||
|
|
||||||
- name: Распаковать релиз ActivityWatch
|
- name: Установить релиз ActivityWatch (download/unarchive/install)
|
||||||
ansible.builtin.unarchive:
|
when: not ansible_check_mode
|
||||||
src: "{{ aw_archive_path }}"
|
block:
|
||||||
dest: "{{ aw_release_dir }}"
|
- name: Скачать архив релиза ActivityWatch
|
||||||
remote_src: true
|
ansible.builtin.get_url:
|
||||||
extra_opts: ["-o"]
|
url: "{{ aw_server_download_url }}"
|
||||||
|
dest: "{{ aw_archive_path }}"
|
||||||
|
mode: "0644"
|
||||||
|
|
||||||
- name: Найти распакованный каталог ActivityWatch
|
- name: Распаковать релиз ActivityWatch
|
||||||
ansible.builtin.find:
|
ansible.builtin.unarchive:
|
||||||
paths: "{{ aw_release_dir }}"
|
src: "{{ aw_archive_path }}"
|
||||||
file_type: directory
|
dest: "{{ aw_release_dir }}"
|
||||||
patterns: "activitywatch*"
|
remote_src: true
|
||||||
register: aw_release_find
|
extra_opts: ["-o"]
|
||||||
|
|
||||||
- name: Найти бинарный файл AW server
|
- name: Найти распакованный каталог ActivityWatch
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
paths: "{{ aw_release_dir }}"
|
paths: "{{ aw_release_dir }}"
|
||||||
file_type: file
|
recurse: true
|
||||||
patterns:
|
file_type: directory
|
||||||
- aw-server-rust
|
patterns: "activitywatch*"
|
||||||
- aw-server
|
register: aw_release_find
|
||||||
register: aw_server_binary_find
|
|
||||||
|
|
||||||
- name: Найти каталог WebUI
|
- name: Найти бинарный файл AW server
|
||||||
ansible.builtin.find:
|
ansible.builtin.find:
|
||||||
paths: "{{ aw_release_dir }}"
|
paths: "{{ aw_release_dir }}"
|
||||||
file_type: directory
|
recurse: true
|
||||||
patterns:
|
file_type: file
|
||||||
- aw-webui
|
patterns:
|
||||||
- webui
|
- aw-server-rust
|
||||||
register: aw_webui_dir_find
|
- aw-server
|
||||||
|
register: aw_server_binary_find
|
||||||
|
|
||||||
- name: Сохранить пути распакованного релиза
|
- name: Найти index.html WebUI
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.find:
|
||||||
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
paths: "{{ aw_release_dir }}"
|
||||||
aw_server_binary_path: "{{ (aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
recurse: true
|
||||||
aw_webui_source_path: "{{ (aw_webui_dir_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
file_type: file
|
||||||
|
patterns:
|
||||||
|
- index.html
|
||||||
|
register: aw_webui_index_find
|
||||||
|
|
||||||
- name: Проверить, что компоненты релиза найдены
|
- name: Сохранить пути распакованного релиза (binary + webui index)
|
||||||
ansible.builtin.assert:
|
ansible.builtin.set_fact:
|
||||||
that:
|
aw_release_extracted: "{{ (aw_release_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list | first) | default('') }}"
|
||||||
- aw_release_extracted is defined
|
aw_server_binary_path: >-
|
||||||
- aw_release_extracted | length > 0
|
{{
|
||||||
- aw_server_binary_path is defined
|
(
|
||||||
- aw_server_binary_path | length > 0
|
(
|
||||||
- aw_webui_source_path is defined
|
(aw_server_binary_find.files | default([]) | sort(attribute='path') | map(attribute='path') | list)
|
||||||
- aw_webui_source_path | length > 0
|
| select('match', '.*/aw-server-rust$') | list | first
|
||||||
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
|
)
|
||||||
|
| 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: Создать каталог установленного релиза
|
- name: Сохранить каталог WebUI (dirname index.html)
|
||||||
ansible.builtin.file:
|
ansible.builtin.set_fact:
|
||||||
path: "{{ aw_release_install_dir }}"
|
aw_webui_source_path: "{{ aw_webui_index_path | dirname }}"
|
||||||
state: directory
|
|
||||||
owner: "{{ aw_server_user }}"
|
|
||||||
group: "{{ aw_server_group }}"
|
|
||||||
mode: "0755"
|
|
||||||
|
|
||||||
- name: Установить бинарный файл AW server
|
- name: Проверить, что компоненты релиза найдены
|
||||||
ansible.builtin.copy:
|
ansible.builtin.assert:
|
||||||
remote_src: true
|
that:
|
||||||
src: "{{ aw_server_binary_path }}"
|
- aw_release_extracted is defined
|
||||||
dest: "{{ aw_release_install_dir }}/aw-server-rust"
|
- aw_release_extracted | length > 0
|
||||||
owner: "{{ aw_server_user }}"
|
- aw_server_binary_path is defined
|
||||||
group: "{{ aw_server_group }}"
|
- aw_server_binary_path | length > 0
|
||||||
mode: "0755"
|
- aw_webui_source_path is defined
|
||||||
|
- aw_webui_source_path | length > 0
|
||||||
|
fail_msg: "Не удалось найти бинарный файл или WebUI в распакованном релизе ActivityWatch."
|
||||||
|
|
||||||
- name: Создать ссылку на активный бинарный файл AW server
|
- name: Создать каталог установленного релиза
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
src: "{{ aw_release_install_dir }}/aw-server-rust"
|
path: "{{ aw_release_install_dir }}"
|
||||||
dest: /opt/activitywatch/bin/aw-server-rust
|
state: directory
|
||||||
owner: "{{ aw_server_user }}"
|
owner: "{{ aw_server_user }}"
|
||||||
group: "{{ aw_server_group }}"
|
group: "{{ aw_server_group }}"
|
||||||
state: link
|
mode: "0755"
|
||||||
force: true
|
|
||||||
|
|
||||||
- name: Синхронизировать WebUI в RU каталог
|
- name: Установить бинарный файл AW server
|
||||||
ansible.builtin.command:
|
ansible.builtin.copy:
|
||||||
cmd: "rsync -a {{ aw_webui_source_path }}/ {{ aw_server_webui_dir }}/"
|
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: Настроить владельца файлов /opt/activitywatch
|
- name: Создать ссылку на активный бинарный файл AW server
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: /opt/activitywatch
|
src: "{{ aw_release_install_dir }}/aw-server-rust"
|
||||||
state: directory
|
dest: /opt/activitywatch/bin/aw-server-rust
|
||||||
owner: "{{ aw_server_user }}"
|
owner: "{{ aw_server_user }}"
|
||||||
group: "{{ aw_server_group }}"
|
group: "{{ aw_server_group }}"
|
||||||
recurse: true
|
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 из шаблона репозитория
|
- name: Установить systemd service из шаблона репозитория
|
||||||
ansible.builtin.copy:
|
ansible.builtin.copy:
|
||||||
@@ -184,143 +236,401 @@
|
|||||||
- Перезагрузить systemd
|
- Перезагрузить systemd
|
||||||
- Перезапустить activitywatch
|
- Перезапустить activitywatch
|
||||||
|
|
||||||
- name: Скопировать RU patch файлы WebUI из репозитория
|
- name: (Check mode) Пропустить WebUI patch и запуск сервиса
|
||||||
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 }}
|
|
||||||
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
|
||||||
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
|
||||||
|
|
||||||
- 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: Перезагрузить systemd после установки AW worktime API
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
daemon_reload: true
|
|
||||||
|
|
||||||
- name: Включить и перезапустить AW worktime API
|
|
||||||
ansible.builtin.systemd:
|
|
||||||
name: aw-worktime-api.service
|
|
||||||
enabled: true
|
|
||||||
state: restarted
|
|
||||||
|
|
||||||
- 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:
|
ansible.builtin.debug:
|
||||||
msg: "apply_webui_ru_patch.sh: {{ apply_ru_patch_result.stdout }}"
|
msg: "ansible_check_mode=true: WebUI patch + service start + API checks are skipped."
|
||||||
|
when: ansible_check_mode
|
||||||
|
|
||||||
- name: Проверить наличие index.html после копирования
|
- name: Применить WebUI RU patch и запустить сервис
|
||||||
ansible.builtin.stat:
|
when: not ansible_check_mode
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
block:
|
||||||
register: aw_webui_ru_index
|
- 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: Проверить, что index.html доступен для RU patch
|
- name: Создать каталог /root/bootstrap для apply_webui_ru_patch.sh
|
||||||
ansible.builtin.assert:
|
ansible.builtin.file:
|
||||||
that:
|
path: /root/bootstrap
|
||||||
- aw_webui_ru_index.stat.exists
|
state: directory
|
||||||
fail_msg: "Не найден index.html WebUI для применения RU patch."
|
mode: "0755"
|
||||||
|
|
||||||
- name: Удалить старые теги RU patch из index.html
|
- name: Скопировать RU patch файлы для apply_webui_ru_patch.sh (хотфиксы compiled JS чанков)
|
||||||
ansible.builtin.replace:
|
ansible.builtin.copy:
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
src: "{{ item.src }}"
|
||||||
regexp: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
dest: "{{ item.dest }}"
|
||||||
replace: ''
|
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: Добавить cleanup script RU patch в index.html
|
- name: Скопировать apply_webui_ru_patch.sh скрипт
|
||||||
ansible.builtin.replace:
|
ansible.builtin.copy:
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
src: "{{ aw_repo_root }}/aw-server/apply_webui_ru_patch.sh"
|
||||||
regexp: '</head>'
|
dest: /opt/activitywatch/aw-server/apply_webui_ru_patch.sh
|
||||||
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
mode: "0755"
|
||||||
|
|
||||||
- name: Добавить загрузчик RU patch перед закрытием body
|
- name: Записать /etc/activitywatch/aw-server.env перед хотфиксами
|
||||||
ansible.builtin.replace:
|
ansible.builtin.copy:
|
||||||
path: "{{ aw_server_webui_dir }}/index.html"
|
dest: /etc/activitywatch/aw-server.env
|
||||||
regexp: '</body>'
|
mode: "0640"
|
||||||
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
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 }}
|
||||||
|
XDG_DATA_HOME={{ aw_server_data_dir }}/.local/share
|
||||||
|
XDG_CONFIG_HOME={{ aw_server_data_dir }}/.config
|
||||||
|
|
||||||
- name: Включить и запустить сервис
|
- name: Установить скрипт AW worktime API
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.copy:
|
||||||
name: activitywatch-server.service
|
src: "{{ aw_repo_root }}/aw-server/aw-worktime-api.py"
|
||||||
enabled: true
|
dest: /usr/local/bin/aw-worktime-api.py
|
||||||
state: restarted
|
owner: root
|
||||||
daemon_reload: true
|
group: root
|
||||||
|
mode: "0755"
|
||||||
|
|
||||||
- name: Дождаться ответа API
|
- 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: Установить 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 после установки AW worktime API
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
daemon_reload: true
|
||||||
|
|
||||||
|
- 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: Применить хотфиксы 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: '<script[^>]+(?:ru-patch-v5\.js|sw-cleanup\.js|aw-ru-patch\.js|aw-sw-cleanup\.js)[^>]*></script>'
|
||||||
|
replace: ''
|
||||||
|
|
||||||
|
- name: Добавить cleanup script RU patch в index.html
|
||||||
|
ansible.builtin.replace:
|
||||||
|
path: "{{ aw_server_webui_dir }}/index.html"
|
||||||
|
regexp: '</head>'
|
||||||
|
replace: '<script src="/js/sw-cleanup.js?v={{ aw_sw_cleanup_cache_bust }}"></script></head>'
|
||||||
|
|
||||||
|
- name: Добавить загрузчик RU patch перед закрытием body
|
||||||
|
ansible.builtin.replace:
|
||||||
|
path: "{{ aw_server_webui_dir }}/index.html"
|
||||||
|
regexp: '</body>'
|
||||||
|
replace: '<script defer="defer" src="/js/ru-patch-v5.js?v={{ aw_ru_patch_cache_bust }}"></script></body>'
|
||||||
|
|
||||||
|
- 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"
|
||||||
|
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)
|
||||||
|
|
||||||
|
- 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:
|
ansible.builtin.uri:
|
||||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/info"
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/"
|
||||||
method: GET
|
method: GET
|
||||||
status_code: 200
|
status_code: 200
|
||||||
register: aw_api
|
register: aw_settings_current
|
||||||
retries: 10
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
delay: 3
|
|
||||||
until: aw_api.status == 200
|
- 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: Применить базовые worktime settings (classes)
|
- name: Применить базовые worktime settings (classes)
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
@@ -342,16 +652,12 @@
|
|||||||
|
|
||||||
- name: Вычислить worktime durationDefault из aw_worktime_from/to
|
- name: Вычислить worktime durationDefault из aw_worktime_from/to
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
aw_worktime_from_h: "{{ (aw_worktime_from | default('08:00')).split(':')[0] | int }}"
|
|
||||||
aw_worktime_from_m: "{{ (aw_worktime_from | default('08:00')).split(':')[1] | int }}"
|
|
||||||
aw_worktime_to_h: "{{ (aw_worktime_to | default('17:00')).split(':')[0] | int }}"
|
|
||||||
aw_worktime_to_m: "{{ (aw_worktime_to | default('17:00')).split(':')[1] | int }}"
|
|
||||||
aw_worktime_duration_default_derived: >-
|
aw_worktime_duration_default_derived: >-
|
||||||
{{
|
{{
|
||||||
(
|
(
|
||||||
(
|
(
|
||||||
((aw_worktime_to_h | int) * 60 + (aw_worktime_to_m | int)) -
|
(((aw_worktime_to | default('17:00')).split(':')[0] | int) * 60 + ((aw_worktime_to | default('17:00')).split(':')[1] | int)) -
|
||||||
((aw_worktime_from_h | int) * 60 + (aw_worktime_from_m | int))
|
(((aw_worktime_from | default('08:00')).split(':')[0] | int) * 60 + ((aw_worktime_from | default('08:00')).split(':')[1] | int))
|
||||||
) * 60
|
) * 60
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -379,20 +685,46 @@
|
|||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/startOfDay"
|
||||||
method: POST
|
method: POST
|
||||||
body: "{{ aw_worktime_start_of_day }}"
|
body: "\"{{ aw_worktime_start_of_day }}\""
|
||||||
body_format: json
|
headers:
|
||||||
status_code: 200
|
Content-Type: application/json
|
||||||
|
status_code: [200, 201]
|
||||||
when: aw_apply_worktime_settings | default(false) | bool
|
when: aw_apply_worktime_settings | default(false) | bool
|
||||||
|
|
||||||
- name: Применить базовый период worktime (durationDefault seconds)
|
- name: Применить базовый период worktime (durationDefault seconds)
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
|
url: "http://127.0.0.1:{{ aw_server_port }}/api/0/settings/durationDefault"
|
||||||
method: POST
|
method: POST
|
||||||
body: "{{ aw_worktime_duration_default_effective }}"
|
body: "{{ aw_worktime_duration_default_effective | string }}"
|
||||||
body_format: json
|
headers:
|
||||||
status_code: 200
|
Content-Type: application/json
|
||||||
|
status_code: [200, 201]
|
||||||
when: aw_apply_worktime_settings | default(false) | bool
|
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
|
||||||
|
|
||||||
handlers:
|
handlers:
|
||||||
- name: Перезагрузить systemd
|
- name: Перезагрузить systemd
|
||||||
ansible.builtin.systemd:
|
ansible.builtin.systemd:
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||||
aw_windows_afk_enabled: true
|
aw_windows_afk_enabled: true
|
||||||
aw_windows_window_enabled: true
|
aw_windows_window_enabled: true
|
||||||
|
aw_windows_file_ops_enabled: true
|
||||||
aw_windows_local_agent_logs_enabled: false
|
aw_windows_local_agent_logs_enabled: false
|
||||||
aw_windows_incident_capture_enabled: true
|
aw_windows_incident_capture_enabled: true
|
||||||
aw_windows_incident_screenshot_enabled: true
|
aw_windows_incident_screenshot_enabled: true
|
||||||
@@ -78,6 +79,7 @@
|
|||||||
- browser-domains-native-collector.ps1
|
- browser-domains-native-collector.ps1
|
||||||
- dlp-endpoint-signals-collector.ps1
|
- dlp-endpoint-signals-collector.ps1
|
||||||
- email-outbound-collector.ps1
|
- email-outbound-collector.ps1
|
||||||
|
- file-operations-collector.ps1
|
||||||
- worktime-session-collector.ps1
|
- worktime-session-collector.ps1
|
||||||
- migrate-awatch-rus-paths.ps1
|
- migrate-awatch-rus-paths.ps1
|
||||||
- deploy-domain-users.ps1
|
- deploy-domain-users.ps1
|
||||||
@@ -87,6 +89,18 @@
|
|||||||
- web-category-rules.example.json
|
- web-category-rules.example.json
|
||||||
- dlp-policy.example.json
|
- dlp-policy.example.json
|
||||||
|
|
||||||
|
- name: Нормализовать кодировку PowerShell файлов (UTF-8 BOM для Windows PowerShell)
|
||||||
|
ansible.windows.win_powershell:
|
||||||
|
script: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$toolkitDir = "{{ aw_windows_deploy_root }}\windows"
|
||||||
|
$encIn = New-Object System.Text.UTF8Encoding($false)
|
||||||
|
$encOut = New-Object System.Text.UTF8Encoding($true)
|
||||||
|
Get-ChildItem -LiteralPath $toolkitDir -File -Include *.ps1,*.psm1,*.psd1 | ForEach-Object {
|
||||||
|
$text = [System.IO.File]::ReadAllText($_.FullName, $encIn)
|
||||||
|
[System.IO.File]::WriteAllText($_.FullName, $text, $encOut)
|
||||||
|
}
|
||||||
|
|
||||||
- name: Загрузить список пользователей для доменного развёртывания
|
- name: Загрузить список пользователей для доменного развёртывания
|
||||||
ansible.windows.win_copy:
|
ansible.windows.win_copy:
|
||||||
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
|
dest: "{{ aw_windows_deploy_root }}\\windows\\users.txt"
|
||||||
@@ -131,6 +145,7 @@
|
|||||||
StateRoot = "{{ aw_windows_state_root }}"
|
StateRoot = "{{ aw_windows_state_root }}"
|
||||||
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
AfkEnabled = {{ '$true' if (aw_windows_afk_enabled | bool) else '$false' }}
|
||||||
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
WindowEnabled = {{ '$true' if (aw_windows_window_enabled | bool) else '$false' }}
|
||||||
|
FileOpsEnabled = {{ '$true' if (aw_windows_file_ops_enabled | bool) else '$false' }}
|
||||||
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
LocalAgentLogsEnabled = {{ '$true' if (aw_windows_local_agent_logs_enabled | bool) else '$false' }}
|
||||||
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
IncidentCaptureEnabled = {{ '$true' if (aw_windows_incident_capture_enabled | bool) else '$false' }}
|
||||||
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
IncidentScreenshotEnabled = {{ '$true' if (aw_windows_incident_screenshot_enabled | bool) else '$false' }}
|
||||||
@@ -150,6 +165,19 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
& "{{ aw_windows_deploy_root }}\windows\deploy-ensemble.ps1" @params
|
||||||
|
|
||||||
|
- name: Удалить лишние ActivityWatch Launch tasks вне текущего deployment-config
|
||||||
|
ansible.windows.win_powershell:
|
||||||
|
script: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$config = Get-Content -Raw -LiteralPath "{{ aw_windows_state_root }}\deployment-config.json" | ConvertFrom-Json
|
||||||
|
$desired = @($config.userTasks | ForEach-Object { [string]$_.LaunchTaskName })
|
||||||
|
foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -like 'ActivityWatch Launch *' })) {
|
||||||
|
if ($desired -notcontains [string]$task.TaskName) {
|
||||||
|
Unregister-ScheduledTask -TaskName $task.TaskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||||
|
& cmd.exe /c "schtasks /Delete /TN `"$($task.TaskName)`" /F >nul 2>&1" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
- name: Принудительно запустить ActivityWatch recovery и launch tasks
|
- name: Принудительно запустить ActivityWatch recovery и launch tasks
|
||||||
when: aw_windows_force_task_restart | bool
|
when: aw_windows_force_task_restart | bool
|
||||||
ansible.windows.win_powershell:
|
ansible.windows.win_powershell:
|
||||||
@@ -172,6 +200,7 @@
|
|||||||
when:
|
when:
|
||||||
- aw_windows_api_smoke_check_enabled | bool
|
- aw_windows_api_smoke_check_enabled | bool
|
||||||
- aw_windows_afk_enabled | bool
|
- aw_windows_afk_enabled | bool
|
||||||
|
- aw_windows_hostname_result.stdout is defined
|
||||||
ansible.builtin.set_fact:
|
ansible.builtin.set_fact:
|
||||||
aw_windows_api_smoke_check_bucket_effective: >-
|
aw_windows_api_smoke_check_bucket_effective: >-
|
||||||
{{
|
{{
|
||||||
@@ -180,54 +209,51 @@
|
|||||||
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
|
else 'aw-watcher-afk_' ~ (aw_windows_hostname_result.stdout | trim)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
- name: Дождаться свежих AFK событий на AW server
|
- name: Выполнить AW API smoke-check (проверка наличия свежих событий в AFK бакете)
|
||||||
when:
|
when:
|
||||||
- aw_windows_api_smoke_check_enabled | bool
|
- aw_windows_api_smoke_check_enabled | bool
|
||||||
- aw_windows_afk_enabled | bool
|
- aw_windows_afk_enabled | bool
|
||||||
delegate_to: localhost
|
|
||||||
ansible.builtin.uri:
|
ansible.builtin.uri:
|
||||||
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
|
url: "{{ aw_windows_server_scheme }}://{{ aw_windows_server_host }}:{{ aw_windows_server_port }}/api/0/buckets/{{ aw_windows_api_smoke_check_bucket_effective }}/events?limit={{ aw_windows_api_smoke_check_limit }}"
|
||||||
method: GET
|
method: GET
|
||||||
return_content: true
|
status_code: 200
|
||||||
register: aw_windows_api_smoke
|
register: aw_windows_api_smoke_result
|
||||||
until: >
|
until: aw_windows_api_smoke_result.json | length > 0
|
||||||
aw_windows_api_smoke.status == 200 and
|
retries: 5
|
||||||
(aw_windows_api_smoke.json | length) > 0 and
|
delay: 5
|
||||||
(
|
ignore_errors: true
|
||||||
aw_windows_api_smoke.json
|
|
||||||
| selectattr('data.status', 'equalto', 'not-afk')
|
|
||||||
| list
|
|
||||||
| length
|
|
||||||
) > 0
|
|
||||||
retries: 10
|
|
||||||
delay: 6
|
|
||||||
|
|
||||||
- name: Выполнить валидацию и сохранить отчёт на целевом Windows host
|
- name: Валидировать развёртывание на эндпоинте
|
||||||
ansible.windows.win_powershell:
|
ansible.windows.win_powershell:
|
||||||
script: |
|
script: |
|
||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$report = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
$result = & "{{ aw_windows_deploy_root }}\windows\validate-deployment.ps1" `
|
||||||
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
|
-ConfigPath "{{ aw_windows_state_root }}\deployment-config.json"
|
||||||
$report | ConvertTo-Json -Depth 12 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
$result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_validation_remote_path }}" -Encoding utf8
|
||||||
if ({{ '$true' if (aw_windows_fail_on_validation_error | bool) else '$false' }} -and -not [bool]$report.overallOk) {
|
return $result
|
||||||
throw "Проверка развёртывания ActivityWatch завершилась ошибкой. Отчёт: {{ aw_windows_validation_remote_path }}"
|
|
||||||
}
|
|
||||||
|
|
||||||
- name: Создать локальный каталог для validation reports
|
- name: Создать локальную директорию для отчётов валидации
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ aw_windows_validation_local_dir }}"
|
path: "{{ aw_windows_validation_local_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
delegate_to: localhost
|
delegate_to: localhost
|
||||||
|
|
||||||
- name: Забрать validation report
|
- name: Стянуть отчёт валидации с эндпоинта
|
||||||
ansible.builtin.fetch:
|
ansible.builtin.fetch:
|
||||||
src: "{{ aw_windows_validation_remote_path }}"
|
src: "{{ aw_windows_validation_remote_path }}"
|
||||||
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
dest: "{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
||||||
flat: true
|
flat: true
|
||||||
|
|
||||||
- name: Показать путь к отчёту
|
- name: Проверить статус валидации
|
||||||
ansible.builtin.debug:
|
ansible.builtin.shell: |
|
||||||
msg:
|
python3 - <<'PY'
|
||||||
- "Windows/RDP развёртывание завершено на {{ inventory_hostname }}."
|
import json, sys
|
||||||
- "Отчёт проверки: {{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json"
|
with open('{{ aw_windows_validation_local_dir }}/{{ inventory_hostname }}-aw_validate_ansible.json', 'r') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not data.get('overallOk', False):
|
||||||
|
print(f"Validation failed for {{ inventory_hostname }}: {data.get('summary', 'Unknown error')}")
|
||||||
|
sys.exit(1)
|
||||||
|
PY
|
||||||
|
delegate_to: localhost
|
||||||
|
when: aw_windows_fail_on_validation_error | bool
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ aw_server_bind_host: "0.0.0.0"
|
|||||||
aw_server_port: 5600
|
aw_server_port: 5600
|
||||||
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
|
aw_server_webui_dir: "/opt/activitywatch/webui-ru"
|
||||||
aw_server_data_dir: "/var/lib/activitywatch"
|
aw_server_data_dir: "/var/lib/activitywatch"
|
||||||
|
aw_server_db_path: "/var/lib/activitywatch/.local/share/activitywatch/aw-server-rust/sqlite.db"
|
||||||
aw_server_log_dir: "/var/log/activitywatch"
|
aw_server_log_dir: "/var/log/activitywatch"
|
||||||
aw_server_user: "activitywatch"
|
aw_server_user: "activitywatch"
|
||||||
aw_server_group: "activitywatch"
|
aw_server_group: "activitywatch"
|
||||||
@@ -12,9 +13,17 @@ aw_worktime_timezone: "Europe/Moscow"
|
|||||||
|
|
||||||
aw_repo_root: "{{ playbook_dir | dirname }}"
|
aw_repo_root: "{{ playbook_dir | dirname }}"
|
||||||
|
|
||||||
# Опционально: применить базовые категории и views для рабочего времени через AW settings API.
|
# Применить базовые категории и views для рабочего времени через AW settings API.
|
||||||
# Внимание: это перезаписывает существующие server-side settings/classes/views.
|
# При прод-обновлениях это нужно оставлять включённым, иначе UI остаётся без views/classes.
|
||||||
aw_apply_worktime_settings: false
|
aw_apply_worktime_settings: true
|
||||||
|
|
||||||
|
# Дополнительные origin для aw-server-rust CORS.
|
||||||
|
# Обязательно включите тот origin, с которого реально открывается Web UI.
|
||||||
|
aw_server_cors_origins:
|
||||||
|
- "http://127.0.0.1:5600"
|
||||||
|
- "http://localhost:5600"
|
||||||
|
- "http://10.10.10.13:5600"
|
||||||
|
- "http://aw-server:5600"
|
||||||
|
|
||||||
# Опциональные значения периода рабочего времени в Web UI.
|
# Опциональные значения периода рабочего времени в Web UI.
|
||||||
# startOfDay задаёт границу дня и стартовое время окна отчёта.
|
# startOfDay задаёт границу дня и стартовое время окна отчёта.
|
||||||
@@ -24,3 +33,5 @@ aw_apply_worktime_settings: false
|
|||||||
aw_worktime_from: "08:00"
|
aw_worktime_from: "08:00"
|
||||||
aw_worktime_to: "17:00"
|
aw_worktime_to: "17:00"
|
||||||
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
|
aw_worktime_start_of_day: "{{ aw_worktime_from }}"
|
||||||
|
aw_server_always_active_pattern: "aw-watcher-window"
|
||||||
|
aw_server_landingpage: "/activity/SHARKON2025/view/"
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ aw_windows_install_root: "C:\\Program Files\\AWatch-rus\\bin"
|
|||||||
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
aw_windows_state_root: "C:\\ProgramData\\AWatch-rus"
|
||||||
aw_windows_afk_enabled: true
|
aw_windows_afk_enabled: true
|
||||||
aw_windows_window_enabled: true
|
aw_windows_window_enabled: true
|
||||||
|
aw_windows_file_ops_enabled: true
|
||||||
aw_windows_local_agent_logs_enabled: false
|
aw_windows_local_agent_logs_enabled: false
|
||||||
aw_windows_incident_capture_enabled: true
|
aw_windows_incident_capture_enabled: true
|
||||||
aw_windows_incident_screenshot_enabled: true
|
aw_windows_incident_screenshot_enabled: true
|
||||||
|
|||||||
@@ -11,6 +11,9 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-ui-bridge.py
|
||||||
|
- aw-worktime-ui-bridge.service
|
||||||
|
- aw-worktime-ui-bridge.timer
|
||||||
- aw-worktime-panel.js
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
|
|||||||
+3
@@ -11,6 +11,9 @@
|
|||||||
- activitywatch-server.service
|
- activitywatch-server.service
|
||||||
- aw-worktime-api.py
|
- aw-worktime-api.py
|
||||||
- aw-worktime-api.service
|
- aw-worktime-api.service
|
||||||
|
- aw-worktime-ui-bridge.py
|
||||||
|
- aw-worktime-ui-bridge.service
|
||||||
|
- aw-worktime-ui-bridge.timer
|
||||||
- aw-worktime-panel.js
|
- aw-worktime-panel.js
|
||||||
- aw-server.env.example
|
- aw-server.env.example
|
||||||
- aw-ru-patch.js
|
- aw-ru-patch.js
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ EnvironmentFile=/etc/activitywatch/aw-server.env
|
|||||||
User=__AW_SERVER_USER__
|
User=__AW_SERVER_USER__
|
||||||
Group=__AW_SERVER_GROUP__
|
Group=__AW_SERVER_GROUP__
|
||||||
WorkingDirectory=__AW_SERVER_DATA_DIR__
|
WorkingDirectory=__AW_SERVER_DATA_DIR__
|
||||||
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT"'
|
ExecStart=/bin/sh -lc 'exec /opt/activitywatch/bin/aw-server-rust --host "$AW_SERVER_BIND_HOST" --port "$AW_SERVER_PORT" --dbpath "$AW_SERVER_DB_PATH" --webpath "$AW_SERVER_WEBUI_DIR"'
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5s
|
RestartSec=5s
|
||||||
StateDirectory=activitywatch
|
StateDirectory=activitywatch
|
||||||
@@ -17,7 +17,7 @@ LogsDirectory=activitywatch
|
|||||||
NoNewPrivileges=true
|
NoNewPrivileges=true
|
||||||
PrivateTmp=true
|
PrivateTmp=true
|
||||||
ProtectSystem=full
|
ProtectSystem=full
|
||||||
ProtectHome=true
|
ProtectHome=read-only
|
||||||
LimitNOFILE=65535
|
LimitNOFILE=65535
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
|
|||||||
@@ -29,6 +29,21 @@
|
|||||||
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
|
{ "label": "DLP", "type": "bucket", "bucket_prefix": "aw-dlp-endpoint-signals_" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "linux-remote",
|
||||||
|
"name": "Linux remote workers",
|
||||||
|
"description": "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||||
|
"patterns": [
|
||||||
|
"^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{ "label": "Активность", "type": "activity" },
|
||||||
|
{ "label": "SSH сессии", "type": "bucket", "bucket_prefix": "aw-ssh-sessions_" },
|
||||||
|
{ "label": "Команды shell", "type": "bucket", "bucket_prefix": "aw-console-commands_" },
|
||||||
|
{ "label": "Web категории", "type": "bucket", "bucket_prefix": "aw-detmir-web-category_" },
|
||||||
|
{ "label": "Все бакеты", "type": "buckets" }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "virtual-infra",
|
"id": "virtual-infra",
|
||||||
"name": "Virtual servers + Proxmox",
|
"name": "Virtual servers + Proxmox",
|
||||||
|
|||||||
@@ -370,6 +370,16 @@
|
|||||||
return /^pve[-_]/i.test(String(host || ""));
|
return /^pve[-_]/i.test(String(host || ""));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isLikelyClientHost(host) {
|
||||||
|
const value = String(host || "").trim();
|
||||||
|
if (!value) return false;
|
||||||
|
if (/^(?:unknown|undefined|null)$/i.test(value)) return false;
|
||||||
|
if (/^(?:localhost|127\.0\.0\.1|0\.0\.0\.0|::1)$/i.test(value)) return false;
|
||||||
|
if (/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)) return false;
|
||||||
|
if (value.indexOf(":") !== -1 && /^[0-9a-f:\[\]]+$/i.test(value)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function enforceSafeActivityViewForPveHost() {
|
function enforceSafeActivityViewForPveHost() {
|
||||||
const hash = window.location.hash || "";
|
const hash = window.location.hash || "";
|
||||||
const match = hash.match(/^#\/activity\/([^/]+)\/day\/([^/]+)\/view\/([^/?#]+)/i);
|
const match = hash.match(/^#\/activity\/([^/]+)\/day\/([^/]+)\/view\/([^/?#]+)/i);
|
||||||
@@ -386,9 +396,9 @@
|
|||||||
|
|
||||||
function getDlpHostFromSettings(settings) {
|
function getDlpHostFromSettings(settings) {
|
||||||
const routeHost = getCurrentHostFromHash();
|
const routeHost = getCurrentHostFromHash();
|
||||||
if (routeHost) return routeHost;
|
if (isLikelyClientHost(routeHost)) return routeHost;
|
||||||
const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash());
|
const bucketHost = getDlpHostFromBucketId(getDlpBucketIdFromHash());
|
||||||
if (bucketHost) return bucketHost;
|
if (isLikelyClientHost(bucketHost)) return bucketHost;
|
||||||
return getTrendsHostFromSettings(settings);
|
return getTrendsHostFromSettings(settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,6 +690,19 @@
|
|||||||
{ label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" }
|
{ label: "DLP", type: "bucket", bucket_prefix: "aw-dlp-endpoint-signals_" }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: "linux-remote",
|
||||||
|
name: "Linux remote workers",
|
||||||
|
description: "Linux-хосты удалённых сотрудников: GUI активность, SSH/console и browser admin UI.",
|
||||||
|
patterns: ["^(LINUX-WS|LINUX-DESKTOP|LX-|DESKTOP-|ADMIN-|WORKSTATION-|DEVBOX-)"],
|
||||||
|
links: [
|
||||||
|
{ label: "Активность", type: "activity" },
|
||||||
|
{ label: "SSH сессии", type: "bucket", bucket_prefix: "aw-ssh-sessions_" },
|
||||||
|
{ label: "Команды shell", type: "bucket", bucket_prefix: "aw-console-commands_" },
|
||||||
|
{ label: "Web категории", type: "bucket", bucket_prefix: "aw-detmir-web-category_" },
|
||||||
|
{ label: "Все бакеты", type: "buckets" }
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: "virtual-infra",
|
id: "virtual-infra",
|
||||||
name: "Virtual servers + Proxmox",
|
name: "Virtual servers + Proxmox",
|
||||||
@@ -740,7 +763,15 @@
|
|||||||
const prefixes = [
|
const prefixes = [
|
||||||
"aw-watcher-window_",
|
"aw-watcher-window_",
|
||||||
"aw-watcher-afk_",
|
"aw-watcher-afk_",
|
||||||
|
"aw-console-commands_",
|
||||||
|
"aw-ssh-sessions_",
|
||||||
|
"aw-linux-web-context_",
|
||||||
|
"aw-detmir-web-category_",
|
||||||
"aw-dlp-endpoint-signals_",
|
"aw-dlp-endpoint-signals_",
|
||||||
|
"aw-session-events_",
|
||||||
|
"aw-worktime-sessions_",
|
||||||
|
"aw-pve-webadmin-events_",
|
||||||
|
"aw-pve-task-events_",
|
||||||
"aw-dlp-incidents_",
|
"aw-dlp-incidents_",
|
||||||
"aw-pfsense-health_",
|
"aw-pfsense-health_",
|
||||||
"aw-pfsense-gateways_",
|
"aw-pfsense-gateways_",
|
||||||
@@ -770,7 +801,27 @@
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchHostGroup(host, groups) {
|
function hostHasBucketPrefix(hostBuckets, prefix) {
|
||||||
|
return (hostBuckets || []).some(function (bucketId) {
|
||||||
|
return String(bucketId || "").indexOf(prefix) === 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchHostGroup(host, groups, hostBuckets) {
|
||||||
|
const bucketList = hostBuckets || [];
|
||||||
|
if (hostHasBucketPrefix(bucketList, "aw-dlp-endpoint-signals_") || hostHasBucketPrefix(bucketList, "aw-session-events_")) {
|
||||||
|
return "windows-rdp";
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-console-commands_") ||
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-ssh-sessions_") ||
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-linux-web-context_") ||
|
||||||
|
hostHasBucketPrefix(bucketList, "aw-detmir-web-category_")
|
||||||
|
) {
|
||||||
|
if (!hostHasBucketPrefix(bucketList, "aw-pve-webadmin-events_") && !hostHasBucketPrefix(bucketList, "aw-pve-task-events_")) {
|
||||||
|
return "linux-remote";
|
||||||
|
}
|
||||||
|
}
|
||||||
for (const group of groups) {
|
for (const group of groups) {
|
||||||
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
const patterns = Array.isArray(group.patterns) ? group.patterns : [];
|
||||||
for (const pattern of patterns) {
|
for (const pattern of patterns) {
|
||||||
@@ -813,7 +864,7 @@
|
|||||||
grouped.set("__ungrouped__", []);
|
grouped.set("__ungrouped__", []);
|
||||||
|
|
||||||
Array.from(hostBuckets.keys()).sort().forEach(function (host) {
|
Array.from(hostBuckets.keys()).sort().forEach(function (host) {
|
||||||
const groupId = matchHostGroup(host, groups) || "__ungrouped__";
|
const groupId = matchHostGroup(host, groups, hostBuckets.get(host) || []) || "__ungrouped__";
|
||||||
grouped.get(groupId).push(host);
|
grouped.get(groupId).push(host);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -869,7 +920,7 @@
|
|||||||
center.setAttribute("data-aw-ru-host-groups", "1");
|
center.setAttribute("data-aw-ru-host-groups", "1");
|
||||||
center.innerHTML =
|
center.innerHTML =
|
||||||
'<h4>Разделы хостов</h4>' +
|
'<h4>Разделы хостов</h4>' +
|
||||||
'<p>Здесь хосты разделены на пользовательские Windows RDP и инфраструктурные виртуальные серверы/Proxmox.</p>' +
|
'<p>Здесь хосты разделены на Windows RDP, Linux remote workers и инфраструктурные узлы.</p>' +
|
||||||
'<div class="aw-ru-host-groups-grid" data-aw-ru-host-groups-grid><section class="aw-ru-host-group-card"><p>Загрузка...</p></section></div>';
|
'<div class="aw-ru-host-groups-grid" data-aw-ru-host-groups-grid><section class="aw-ru-host-group-card"><p>Загрузка...</p></section></div>';
|
||||||
heading.parentElement.insertBefore(center, heading.nextSibling);
|
heading.parentElement.insertBefore(center, heading.nextSibling);
|
||||||
}
|
}
|
||||||
@@ -1425,7 +1476,8 @@
|
|||||||
if (!settings || typeof settings !== "object") return "";
|
if (!settings || typeof settings !== "object") return "";
|
||||||
const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : "";
|
const landingpage = typeof settings.landingpage === "string" ? settings.landingpage : "";
|
||||||
const match = landingpage.match(/\/activity\/([^/]+)/);
|
const match = landingpage.match(/\/activity\/([^/]+)/);
|
||||||
return match && match[1] ? match[1] : "";
|
const host = match && match[1] ? decodeURIComponent(match[1]) : "";
|
||||||
|
return isLikelyClientHost(host) ? host : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTrendsPath(hash) {
|
function getTrendsPath(hash) {
|
||||||
@@ -1492,8 +1544,7 @@
|
|||||||
.map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); })
|
.map(function (bucketId) { return bucketId.replace(/^aw-watcher-window_/i, ""); })
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.filter(function (host) { return !/^unknown$/i.test(host); });
|
.filter(function (host) { return !/^unknown$/i.test(host); });
|
||||||
if (settingsHost && hosts.indexOf(settingsHost) >= 0) return settingsHost;
|
if (isLikelyClientHost(settingsHost) && hosts.indexOf(settingsHost) >= 0) return settingsHost;
|
||||||
if (settingsHost) return settingsHost;
|
|
||||||
hosts.sort();
|
hosts.sort();
|
||||||
return hosts[0] || "";
|
return hosts[0] || "";
|
||||||
}
|
}
|
||||||
@@ -1533,7 +1584,8 @@
|
|||||||
window.fetch = function (input, init) {
|
window.fetch = function (input, init) {
|
||||||
try {
|
try {
|
||||||
const url = typeof input === "string" ? input : String(input && input.url || "");
|
const url = typeof input === "string" ? input : String(input && input.url || "");
|
||||||
if (/\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") {
|
const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "");
|
||||||
|
if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && init && typeof init.body === "string") {
|
||||||
init = Object.assign({}, init, {
|
init = Object.assign({}, init, {
|
||||||
body: rewriteUnknownCategoryBuilderQueryBody(init.body)
|
body: rewriteUnknownCategoryBuilderQueryBody(init.body)
|
||||||
});
|
});
|
||||||
@@ -1557,7 +1609,8 @@
|
|||||||
proto.send = function (body) {
|
proto.send = function (body) {
|
||||||
try {
|
try {
|
||||||
const url = String(this.__awRuUrl || "");
|
const url = String(this.__awRuUrl || "");
|
||||||
if (/\/api\/0\/query\/?$/i.test(url) && typeof body === "string") {
|
const isCategoryBuilderRoute = /^#\/settings\/category-builder(?:[/?#]|$)/i.test(window.location.hash || "");
|
||||||
|
if (isCategoryBuilderRoute && /\/api\/0\/query\/?$/i.test(url) && typeof body === "string") {
|
||||||
body = rewriteUnknownCategoryBuilderQueryBody(body);
|
body = rewriteUnknownCategoryBuilderQueryBody(body);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ VIEWS_JSON="$BOOTSTRAP_DIR/settings/views-default.json"
|
|||||||
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
CLASSES_JSON="$BOOTSTRAP_DIR/settings/classes-worktime.json"
|
||||||
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
WORKTIME_API_SRC="$BOOTSTRAP_DIR/aw-worktime-api.py"
|
||||||
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
WORKTIME_API_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-api.service"
|
||||||
|
WORKTIME_UI_BRIDGE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.py"
|
||||||
|
WORKTIME_UI_BRIDGE_SERVICE_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.service"
|
||||||
|
WORKTIME_UI_BRIDGE_TIMER_SRC="$BOOTSTRAP_DIR/aw-worktime-ui-bridge.timer"
|
||||||
|
|
||||||
for var_name in "${required_vars[@]}"; do
|
for var_name in "${required_vars[@]}"; do
|
||||||
if [[ -z "${!var_name:-}" ]]; then
|
if [[ -z "${!var_name:-}" ]]; then
|
||||||
@@ -103,6 +106,24 @@ if [[ -f "$WORKTIME_API_SERVICE_SRC" ]]; then
|
|||||||
systemctl --no-pager --full status aw-worktime-api.service || true
|
systemctl --no-pager --full status aw-worktime-api.service || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$WORKTIME_UI_BRIDGE_SRC" ]]; then
|
||||||
|
install -m 0755 "$WORKTIME_UI_BRIDGE_SRC" /usr/local/bin/aw-worktime-ui-bridge.py
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$WORKTIME_UI_BRIDGE_SERVICE_SRC" ]]; then
|
||||||
|
install -m 0644 "$WORKTIME_UI_BRIDGE_SERVICE_SRC" /etc/systemd/system/aw-worktime-ui-bridge.service
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "$WORKTIME_UI_BRIDGE_TIMER_SRC" ]]; then
|
||||||
|
install -m 0644 "$WORKTIME_UI_BRIDGE_TIMER_SRC" /etc/systemd/system/aw-worktime-ui-bridge.timer
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl disable --now aw-worktime-afk-bridge.timer >/dev/null 2>&1 || true
|
||||||
|
systemctl enable aw-worktime-ui-bridge.timer
|
||||||
|
systemctl restart aw-worktime-ui-bridge.timer
|
||||||
|
systemctl start aw-worktime-ui-bridge.service || true
|
||||||
|
systemctl --no-pager --full status aw-worktime-ui-bridge.timer || true
|
||||||
|
fi
|
||||||
|
|
||||||
for _ in $(seq 1 20); do
|
for _ in $(seq 1 20); do
|
||||||
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
if curl -fsS "http://127.0.0.1:${AW_SERVER_PORT}/api/0/info" >/dev/null 2>&1; then
|
||||||
break
|
break
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"name": ["Работа", "Документы"],
|
"name": ["Работа", "Документы"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64)\\.exe\\b|Adobe Reader|Acrobat",
|
"regex": "\\b(winword|excel|powerpnt|outlook|acrord32|acrord64|libreoffice|writer|calc)\\.exe\\b|LibreOffice|OnlyOffice|Adobe Reader|Acrobat",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#2E7D32" }
|
"data": { "color": "#2E7D32" }
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
"name": ["Работа", "Администрирование"],
|
"name": ["Работа", "Администрирование"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd)\\.exe\\b",
|
"regex": "\\b(mstsc|putty|kitty|winscp|anydesk|teamviewer|vncviewer|mmc|regedit|services|control|powershell|cmd|gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\.exe\\b|\\b(gnome-terminal|gnome-terminal-server|xfce4-terminal|konsole|tilix|alacritty|xterm|remmina|virt-manager)\\b|Proxmox Virtual Environment|\\bpfSense\\b|\\bGrafana\\b|\\bKibana\\b|\\bPortainer\\b",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#6D4C41" }
|
"data": { "color": "#6D4C41" }
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
"name": ["Интернет", "Браузер"],
|
"name": ["Интернет", "Браузер"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser)\\.exe\\b",
|
"regex": "\\b(chrome|msedge|firefox|opera|brave|vivaldi|browser|chromium)\\.exe\\b|\\b(chrome|chromium|firefox|opera|brave|vivaldi)\\b",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": { "color": "#00897B" }
|
"data": { "color": "#00897B" }
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
"name": ["ActivityWatch"],
|
"name": ["ActivityWatch"],
|
||||||
"rule": {
|
"rule": {
|
||||||
"type": "regex",
|
"type": "regex",
|
||||||
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b",
|
"regex": "ActivityWatch|\\baw-(watcher|qt)\\.exe\\b|\\baw-(watcher|qt)\\b",
|
||||||
"ignore_case": true
|
"ignore_case": true
|
||||||
},
|
},
|
||||||
"data": {}
|
"data": {}
|
||||||
|
|||||||
@@ -306,6 +306,9 @@ function Copy-ActivityWatchCollectorAssets {
|
|||||||
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
|
$resolvedRules = Resolve-Path -LiteralPath $CustomRulesSource -ErrorAction Stop
|
||||||
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
|
Copy-Item -LiteralPath $resolvedRules.Path -Destination $rulesTarget -Force
|
||||||
}
|
}
|
||||||
|
else {
|
||||||
|
Copy-Item -LiteralPath $exampleRulesTarget -Destination $rulesTarget -Force
|
||||||
|
}
|
||||||
|
|
||||||
if ($CustomPolicySource) {
|
if ($CustomPolicySource) {
|
||||||
$resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop
|
$resolvedPolicy = Resolve-Path -LiteralPath $CustomPolicySource -ErrorAction Stop
|
||||||
@@ -534,11 +537,7 @@ function Get-CollectorPowerShellProcessCount {
|
|||||||
function New-LaunchLock {
|
function New-LaunchLock {
|
||||||
param([string]`$StateRoot, [int]`$SessionId)
|
param([string]`$StateRoot, [int]`$SessionId)
|
||||||
|
|
||||||
if (-not (Test-Path -LiteralPath `$StateRoot)) {
|
`$lockPath = Join-Path `$env:TEMP ("launch-watchers-session-{0}.lock" -f `$SessionId)
|
||||||
New-Item -Path `$StateRoot -ItemType Directory -Force | Out-Null
|
|
||||||
}
|
|
||||||
|
|
||||||
`$lockPath = Join-Path `$StateRoot ("launch-watchers-session-{0}.lock" -f `$SessionId)
|
|
||||||
if (Test-Path -LiteralPath `$lockPath) {
|
if (Test-Path -LiteralPath `$lockPath) {
|
||||||
try {
|
try {
|
||||||
`$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json
|
`$lockData = Get-Content -LiteralPath `$lockPath -Raw | ConvertFrom-Json
|
||||||
@@ -731,13 +730,11 @@ function Start-CollectorScriptIfNeeded {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Start-Process -FilePath `$PowerShellExe -ArgumentList @(
|
`$staParam = if (`$ScriptPath -like "*endpoint-signals*") { "-STA" } else { `$null }
|
||||||
'-NoProfile',
|
`$argumentList = @('-NoProfile', '-WindowStyle', 'Hidden', '-ExecutionPolicy', 'Bypass')
|
||||||
'-WindowStyle', 'Hidden',
|
if (`$staParam) { `$argumentList += `$staParam }
|
||||||
'-ExecutionPolicy', 'Bypass',
|
`$argumentList += @('-File', `$ScriptPath, '-ConfigPath', `$ConfigPath)
|
||||||
'-File', `$ScriptPath,
|
Start-Process -FilePath `$PowerShellExe -ArgumentList `$argumentList -WindowStyle Hidden
|
||||||
'-ConfigPath', `$ConfigPath
|
|
||||||
) -WindowStyle Hidden
|
|
||||||
}
|
}
|
||||||
|
|
||||||
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
`$config = Get-DeploymentConfig -Path `$ConfigPath
|
||||||
|
|||||||
+3354
-5
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -112,8 +112,8 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'ActivityWatch развёрнут для пользователей:'
|
Write-Output 'ActivityWatch развёрнут для пользователей:'
|
||||||
$targetUsers | ForEach-Object { Write-Host " - $_" }
|
$targetUsers | ForEach-Object { Write-Output " - $_" }
|
||||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||||
Write-Host "Каталог данных: $StateRoot"
|
Write-Output "Каталог данных: $StateRoot"
|
||||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -136,6 +136,6 @@ if ($reportDirectory) {
|
|||||||
|
|
||||||
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
$report | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $effectiveReportPath -Encoding UTF8
|
||||||
|
|
||||||
Write-Host 'Комплексное развёртывание ActivityWatch завершено.'
|
Write-Output 'Комплексное развёртывание ActivityWatch завершено.'
|
||||||
Write-Host "Пользователи: $($resolvedUsers -join ', ')"
|
Write-Output "Пользователи: $($resolvedUsers -join ', ')"
|
||||||
Write-Host "Отчёт: $effectiveReportPath"
|
Write-Output "Отчёт: $effectiveReportPath"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)]
|
[Parameter(Mandatory = $true)]
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -104,9 +104,9 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $recoveryScriptPath -ConfigPath $configPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host "ActivityWatch развёрнут для пользователя: $TargetUser"
|
Write-Output "ActivityWatch развёрнут для пользователя: $TargetUser"
|
||||||
Write-Host "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
Write-Output "Сервер: ${ServerScheme}://$ServerHost`:$ServerPort"
|
||||||
Write-Host "Каталог установки: $InstallRoot"
|
Write-Output "Каталог установки: $InstallRoot"
|
||||||
Write-Host "Каталог данных: $StateRoot"
|
Write-Output "Каталог данных: $StateRoot"
|
||||||
Write-Host "Файл правил: $($assetResult.ActiveRules)"
|
Write-Output "Файл правил: $($assetResult.ActiveRules)"
|
||||||
Write-Host "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
Write-Output "Файл DLP-политики: $($assetResult.ActivePolicy)"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$ServerHost,
|
[string]$ServerHost,
|
||||||
@@ -151,6 +151,6 @@ Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptP
|
|||||||
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $effectiveRecoveryScript -ConfigPath $effectiveConfigPath
|
||||||
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName
|
||||||
|
|
||||||
Write-Host 'Укрепление и восстановление ActivityWatch завершены.'
|
Write-Output 'Укрепление и восстановление ActivityWatch завершены.'
|
||||||
Write-Host "Конфигурация: $effectiveConfigPath"
|
Write-Output "Конфигурация: $effectiveConfigPath"
|
||||||
Write-Host "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
Write-Output "Пользователи восстановлены: $($effectiveUsers -join ', ')"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||||
param(
|
param(
|
||||||
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
[string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2',
|
||||||
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
[string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2',
|
||||||
@@ -154,7 +154,36 @@ if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch W
|
|||||||
@{ Source = $NewStateRoot; Name = 'new-state' }
|
@{ Source = $NewStateRoot; Name = 'new-state' }
|
||||||
)) {
|
)) {
|
||||||
if (Test-Path -LiteralPath $item.Source) {
|
if (Test-Path -LiteralPath $item.Source) {
|
||||||
Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force
|
$backupDest = Join-Path $backupRoot $item.Name
|
||||||
|
New-ActivityWatchDirectory -Path $backupDest
|
||||||
|
|
||||||
|
$excludeDirs = @()
|
||||||
|
if ($item.Source -eq $NewStateRoot) {
|
||||||
|
# Avoid infinite recursion: backupRoot is inside NewStateRoot by default.
|
||||||
|
$excludeDirs += $backupRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
$robocopyArgs = @(
|
||||||
|
$item.Source,
|
||||||
|
$backupDest,
|
||||||
|
'/E',
|
||||||
|
'/R:1',
|
||||||
|
'/W:1',
|
||||||
|
'/NFL',
|
||||||
|
'/NDL',
|
||||||
|
'/NJH',
|
||||||
|
'/NJS',
|
||||||
|
'/NP'
|
||||||
|
)
|
||||||
|
if ($excludeDirs.Count -gt 0) {
|
||||||
|
$robocopyArgs += '/XD'
|
||||||
|
$robocopyArgs += $excludeDirs
|
||||||
|
}
|
||||||
|
|
||||||
|
& robocopy @robocopyArgs | Out-Null
|
||||||
|
if ($LASTEXITCODE -ge 8) {
|
||||||
|
throw "Backup robocopy failed (exit=$LASTEXITCODE) for source '$($item.Source)' to '$backupDest'"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
|
[string]$ConfigPath = 'C:\ProgramData\ActivityWatch\deployment-config.json'
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -13,49 +13,26 @@ $config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
|
|||||||
$installRoot = [string]$config.paths.installRoot
|
$installRoot = [string]$config.paths.installRoot
|
||||||
$stateRoot = [string]$config.paths.stateRoot
|
$stateRoot = [string]$config.paths.stateRoot
|
||||||
$collectorScript = [string]$config.paths.collectorScript
|
$collectorScript = [string]$config.paths.collectorScript
|
||||||
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
|
|
||||||
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
|
|
||||||
$rulesPath = [string]$config.paths.rulesPath
|
$rulesPath = [string]$config.paths.rulesPath
|
||||||
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
|
|
||||||
$launchScript = [string]$config.paths.launchScript
|
$launchScript = [string]$config.paths.launchScript
|
||||||
$recoveryScript = [string]$config.paths.recoveryScript
|
$recoveryScript = [string]$config.paths.recoveryScript
|
||||||
|
|
||||||
$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
|
|
||||||
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
|
|
||||||
$requiredFiles = @(
|
$requiredFiles = @(
|
||||||
|
(Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe'),
|
||||||
|
(Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe'),
|
||||||
$collectorScript,
|
$collectorScript,
|
||||||
$endpointCollectorScript,
|
|
||||||
$sessionCollectorScript,
|
|
||||||
$rulesPath,
|
$rulesPath,
|
||||||
$policyPath,
|
|
||||||
$launchScript,
|
$launchScript,
|
||||||
$recoveryScript,
|
$recoveryScript,
|
||||||
$ConfigPath
|
$ConfigPath
|
||||||
)
|
)
|
||||||
if ($afkExpected) {
|
|
||||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
|
|
||||||
}
|
|
||||||
if ($windowExpected) {
|
|
||||||
$requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
|
|
||||||
}
|
|
||||||
|
|
||||||
$missingFiles = @(
|
$missingFiles = @(
|
||||||
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
$requiredFiles | Where-Object { -not (Test-Path -LiteralPath $_) }
|
||||||
)
|
)
|
||||||
|
|
||||||
$processNames = @()
|
$processNames = @('aw-watcher-afk', 'aw-watcher-window')
|
||||||
if ($afkExpected) { $processNames += 'aw-watcher-afk' }
|
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
||||||
if ($windowExpected) { $processNames += 'aw-watcher-window' }
|
|
||||||
$runningProcesses = @()
|
|
||||||
if ($processNames.Count -gt 0) {
|
|
||||||
$runningProcesses = Get-Process -Name $processNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId
|
|
||||||
}
|
|
||||||
$sessionCollectorProcesses = Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object {
|
|
||||||
($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
|
|
||||||
$_.CommandLine -match [Regex]::Escape($sessionCollectorScript)
|
|
||||||
} |
|
|
||||||
Select-Object Name, ProcessId, SessionId, CommandLine
|
|
||||||
|
|
||||||
$taskNames = @()
|
$taskNames = @()
|
||||||
if ($config.userTasks) {
|
if ($config.userTasks) {
|
||||||
@@ -65,7 +42,7 @@ $taskNames += [string]$config.recovery.taskName
|
|||||||
$taskNames = $taskNames | Sort-Object -Unique
|
$taskNames = $taskNames | Sort-Object -Unique
|
||||||
|
|
||||||
$tasks = foreach ($taskName in $taskNames) {
|
$tasks = foreach ($taskName in $taskNames) {
|
||||||
$task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1
|
$task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
|
||||||
if ($task) {
|
if ($task) {
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
taskName = $task.TaskName
|
taskName = $task.TaskName
|
||||||
@@ -76,7 +53,7 @@ $tasks = foreach ($taskName in $taskNames) {
|
|||||||
else {
|
else {
|
||||||
[pscustomobject]@{
|
[pscustomobject]@{
|
||||||
taskName = $taskName
|
taskName = $taskName
|
||||||
state = 'Отсутствует'
|
state = 'Missing'
|
||||||
present = $false
|
present = $false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -99,16 +76,8 @@ $result = [ordered]@{
|
|||||||
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.present }))
|
||||||
}
|
}
|
||||||
processes = [ordered]@{
|
processes = [ordered]@{
|
||||||
expected = $processNames
|
|
||||||
list = @($runningProcesses)
|
list = @($runningProcesses)
|
||||||
sessionCollectors = @($sessionCollectorProcesses)
|
ok = [bool](($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge 2)
|
||||||
ok = [bool](
|
|
||||||
(
|
|
||||||
($processNames.Count -eq 0) -or
|
|
||||||
(($runningProcesses | Select-Object -ExpandProperty Name -Unique).Count -ge $processNames.Count)
|
|
||||||
) -and
|
|
||||||
($sessionCollectorProcesses.Count -ge 1)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,44 @@
|
|||||||
param(
|
param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
[string]$Hostname,
|
[string]$Hostname,
|
||||||
[int]$PollSeconds = 30
|
[int]$PollSeconds = 30
|
||||||
@@ -47,7 +87,12 @@ function Ensure-Bucket {
|
|||||||
hostname = $HostnameValue
|
hostname = $HostnameValue
|
||||||
} | ConvertTo-Json -Compress
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Get-SessionRecords {
|
function Get-SessionRecords {
|
||||||
@@ -159,3 +204,458 @@ while ($true) {
|
|||||||
|
|
||||||
Start-Sleep -Seconds $sleepSec
|
Start-Sleep -Seconds $sleepSec
|
||||||
}
|
}
|
||||||
|
; }
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
; }
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch { Write-Error param(
|
||||||
|
[string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json',
|
||||||
|
[string]$Hostname,
|
||||||
|
[int]$PollSeconds = 30
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
function Get-Config {
|
||||||
|
param([string]$Path)
|
||||||
|
|
||||||
|
if (-not (Test-Path -LiteralPath $Path)) {
|
||||||
|
throw "Конфигурация не найдена: $Path"
|
||||||
|
}
|
||||||
|
|
||||||
|
Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-AwJsonPost {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
$bytes = [Text.Encoding]::UTF8.GetBytes($Json)
|
||||||
|
Invoke-RestMethod -Method Post -Uri $Uri -ContentType 'application/json; charset=utf-8' -Body $bytes | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
function Ensure-Bucket {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$ApiBase,
|
||||||
|
[Parameter(Mandatory = $true)][string]$BucketId,
|
||||||
|
[Parameter(Mandatory = $true)][string]$HostnameValue
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
return
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = @{
|
||||||
|
client = 'aw-worktime-session-collector'
|
||||||
|
type = 'aw.worktime.session'
|
||||||
|
hostname = $HostnameValue
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$ApiBase/buckets/$BucketId" -Json $body
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Invoke-RestMethod -Method Get -Uri "$ApiBase/buckets/$BucketId" | Out-Null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SessionRecords {
|
||||||
|
$records = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
$lines = quser 2>$null
|
||||||
|
if (-not $lines) {
|
||||||
|
return @()
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($line in ($lines | Select-Object -Skip 1)) {
|
||||||
|
$clean = ($line -replace '^\s*>?', '').Trim()
|
||||||
|
if (-not $clean) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $clean -split '\s+'
|
||||||
|
if ($parts.Count -lt 4) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionName = ''
|
||||||
|
$sessionIdIndex = 2
|
||||||
|
if ($parts[1] -match '^\d+$') {
|
||||||
|
$sessionIdIndex = 1
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$sessionName = $parts[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
$sessionId = 0
|
||||||
|
if ($parts[$sessionIdIndex] -match '^\d+$') {
|
||||||
|
$sessionId = [int]$parts[$sessionIdIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
$records += [pscustomobject]@{
|
||||||
|
username = $parts[0]
|
||||||
|
sessionName = $sessionName
|
||||||
|
sessionId = $sessionId
|
||||||
|
state = $parts[$sessionIdIndex + 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
|
||||||
|
return $records
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SessionIsActive {
|
||||||
|
param([AllowNull()][string]$State)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($State)) { return $false }
|
||||||
|
$s = $State.Trim().ToLowerInvariant()
|
||||||
|
return ($s -eq 'active') -or ($s -like 'актив*')
|
||||||
|
}
|
||||||
|
|
||||||
|
$cfg = Get-Config -Path $ConfigPath
|
||||||
|
$hostValue = if ($Hostname) { $Hostname } else { [string]$env:COMPUTERNAME }
|
||||||
|
$apiBase = '{0}://{1}:{2}/api/0' -f [string]$cfg.server.scheme, [string]$cfg.server.host, [string]$cfg.server.port
|
||||||
|
$bucketId = 'aw-worktime-sessions_' + $hostValue
|
||||||
|
$pulse = 120
|
||||||
|
$sleepSec = if ($PollSeconds -gt 0) {
|
||||||
|
$PollSeconds
|
||||||
|
}
|
||||||
|
elseif ($cfg.collector -and $cfg.collector.pollSeconds) {
|
||||||
|
[int]$cfg.collector.pollSeconds
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
30
|
||||||
|
}
|
||||||
|
|
||||||
|
Ensure-Bucket -ApiBase $apiBase -BucketId $bucketId -HostnameValue $hostValue
|
||||||
|
|
||||||
|
while ($true) {
|
||||||
|
$now = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
|
||||||
|
$records = Get-SessionRecords
|
||||||
|
if (-not $records -or $records.Count -eq 0) {
|
||||||
|
$records = @([pscustomobject]@{
|
||||||
|
username = $env:USERNAME
|
||||||
|
sessionName = ''
|
||||||
|
sessionId = (Get-Process -Id $PID).SessionId
|
||||||
|
state = 'Unknown'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($rec in $records) {
|
||||||
|
$payload = @{
|
||||||
|
timestamp = $now
|
||||||
|
duration = 0
|
||||||
|
data = @{
|
||||||
|
username = [string]$rec.username
|
||||||
|
userId = "$($env:USERDOMAIN)\$($rec.username)"
|
||||||
|
sessionId = [int]$rec.sessionId
|
||||||
|
sessionName = [string]$rec.sessionName
|
||||||
|
state = [string]$rec.state
|
||||||
|
active = (Test-SessionIsActive -State ([string]$rec.state))
|
||||||
|
hostname = $hostValue
|
||||||
|
source = 'worktime-session-collector'
|
||||||
|
}
|
||||||
|
} | ConvertTo-Json -Depth 6 -Compress
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-AwJsonPost -Uri "$apiBase/buckets/$bucketId/heartbeat?pulsetime=$pulse" -Json $payload
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
; }
|
||||||
|
}
|
||||||
|
|
||||||
|
Start-Sleep -Seconds $sleepSec
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user