diff --git a/ansible/README.md b/ansible/README.md index d642c98..b22fc6c 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -93,6 +93,7 @@ ansible-playbook -i inventory.ini deploy_aw_windows.yml Playbook: - выгружает полный `windows/*` toolkit на целевой хост в InnoSetup-compatible каталог `C:\Program Files\AWatch-rus\windows`, включая DLP и `worktime-session-collector.ps1`; +- если найден legacy config `C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`, выполняет безопасную миграцию через `migrate-awatch-rus-paths.ps1`: backup, остановка задач, перенос данных, переписывание путей, пересоздание scheduled tasks и validation; - выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с policy/rules из AWatch-rus toolkit; - после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи; - выполняет API smoke-check bucket `aw-watcher-afk_` и ожидает свежие `not-afk` события; @@ -110,6 +111,9 @@ Playbook: - `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`; - `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`; - `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`; +- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`; +- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос; +- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте; - `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch; - `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_`; - `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`; diff --git a/ansible/deploy_aw_windows.yml b/ansible/deploy_aw_windows.yml index 41d4657..5d18277 100644 --- a/ansible/deploy_aw_windows.yml +++ b/ansible/deploy_aw_windows.yml @@ -42,6 +42,10 @@ aw_windows_api_smoke_check_bucket: "" aw_windows_api_smoke_check_limit: 10 aw_windows_fail_on_validation_error: true + aw_windows_migration_enabled: true + aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2" + aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" + aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json" tasks: - name: Проверить обязательные переменные @@ -74,6 +78,7 @@ - browser-domains-native-collector.ps1 - dlp-endpoint-signals-collector.ps1 - worktime-session-collector.ps1 + - migrate-awatch-rus-paths.ps1 - deploy-domain-users.ps1 - deploy-ensemble.ps1 - hardening-recovery.ps1 @@ -89,6 +94,27 @@ {{ user }} {% endfor -%} + - name: Проверить нужен ли migration с legacy ActivityWatch путей + when: aw_windows_migration_enabled | bool + ansible.windows.win_stat: + path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json" + register: aw_windows_legacy_config + + - name: Выполнить безопасную migration legacy prod в AWatch-rus + when: + - aw_windows_migration_enabled | bool + - aw_windows_legacy_config.stat.exists | default(false) + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" ` + -OldInstallRoot "{{ aw_windows_legacy_install_root }}" ` + -OldStateRoot "{{ aw_windows_legacy_state_root }}" ` + -NewInstallRoot "{{ aw_windows_install_root }}" ` + -NewStateRoot "{{ aw_windows_state_root }}" ` + -ToolkitRoot "{{ aw_windows_deploy_root }}\windows" + $result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8 + - name: Запустить Windows/RDP ensemble развёртывание ansible.windows.win_powershell: script: | diff --git a/ansible/group_vars/windows.example.yml b/ansible/group_vars/windows.example.yml index ad29b15..30c20c3 100644 --- a/ansible/group_vars/windows.example.yml +++ b/ansible/group_vars/windows.example.yml @@ -37,6 +37,12 @@ aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ans aw_windows_validation_local_dir: "/tmp/aw-rus-validation" aw_windows_fail_on_validation_error: true +# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus. +aw_windows_migration_enabled: true +aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2" +aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" +aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json" + # По умолчанию AFK bucket вычисляется как aw-watcher-afk_. # Задайте явное значение только если watcher пишет в нестандартный bucket. aw_windows_api_smoke_check_enabled: true diff --git a/docs/FULL_DEPLOYMENT_MANUAL_RU.md b/docs/FULL_DEPLOYMENT_MANUAL_RU.md index 704e66a..d850ac9 100755 --- a/docs/FULL_DEPLOYMENT_MANUAL_RU.md +++ b/docs/FULL_DEPLOYMENT_MANUAL_RU.md @@ -187,6 +187,19 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process ### 3.2 Массовое доменное развёртывание (рекомендуется) +Если текущий production ещё работает в старых каталогах +`C:\Program Files\ActivityWatch-Phase2` и `C:\ProgramData\ActivityWatch-Phase2`, +сначала выполните безопасную миграцию: + +```powershell +C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 -WhatIf +C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 +``` + +Скрипт остановит `ActivityWatch Recovery`/`ActivityWatch Launch *`, создаст backup в +`C:\ProgramData\AWatch-rus\migration-backups\...`, перенесёт файлы в единые пути, +пересоздаст `deployment-config.json`/scheduled tasks и запустит validation. + Пример со списком пользователей: ```powershell diff --git a/docs/windows/deployment.md b/docs/windows/deployment.md index 9b35daa..23b3a2d 100755 --- a/docs/windows/deployment.md +++ b/docs/windows/deployment.md @@ -143,9 +143,37 @@ CSV-формат: колонка `User`, `Username`, `SamAccountName` или `Lo Если список уже содержит `DOMAIN\user`, параметр `-Domain` не нужен. -## Рекомендуемый phased rollout (изолированный профиль) +## Безопасная миграция текущего production -Для безопасного параллельного запуска рядом с legacy-инсталляцией используйте отдельные пути: +Если текущий RDP production уже работает в `C:\Program Files\ActivityWatch-Phase2` и +`C:\ProgramData\ActivityWatch-Phase2`, не запускайте обычный update без миграции. +Сначала выполните перенос в единый профиль AWatch-rus: + +```powershell +C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 -WhatIf +C:\Program Files\AWatch-rus\windows\migrate-awatch-rus-paths.ps1 +``` + +Скрипт делает безопасный порядок: + +1. Находит старый `deployment-config.json`. +2. Останавливает `ActivityWatch Recovery` и `ActivityWatch Launch *`. +3. Создаёт backup старых и новых каталогов в + `C:\ProgramData\AWatch-rus\migration-backups\YYYYMMDD-HHMMSS`. +4. Копирует бинарники/состояние в единые пути: + - `C:\Program Files\AWatch-rus\bin` + - `C:\ProgramData\AWatch-rus` +5. Переписывает пути в `deployment-config.json`. +6. Пересоздаёт launcher/recovery scripts и scheduled tasks. +7. Запускает `validate-deployment.ps1`; при ошибке оставляет backup path в сообщении. + +Ansible playbook `ansible/deploy_aw_windows.yml` выполняет этот migration guard +автоматически, если на хосте найден +`C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`. + +## Рекомендуемый rollout + +Для запуска после миграции используйте единые пути: ```powershell .\windows\deploy-domain-users.ps1 ` @@ -207,7 +235,7 @@ Single-user pilot в таком же стиле: - `C:\ProgramData\AWatch-rus\dlp-policy.json` — активная DLP-политика. - `C:\ProgramData\AWatch-rus\logs\` — логи collector'а. -Для phased rollout те же файлы формируются в каталоге `StateRoot`, переданном параметром. +При переопределении `StateRoot` те же файлы формируются в указанном каталоге. ## Повторный прогон diff --git a/install-kit-awindows-20260427-211240/MANIFEST.txt b/install-kit-awindows-20260427-211240/MANIFEST.txt index 0eedf4a..4f5c2f0 100644 --- a/install-kit-awindows-20260427-211240/MANIFEST.txt +++ b/install-kit-awindows-20260427-211240/MANIFEST.txt @@ -1,13 +1,13 @@ 0754dcba7c651d67a40e09446868d2fcae623a100d4fb01794dd96272d353b49 install-kit-awindows-20260427-211240/README-INSTALL-KIT.txt -8d239c67078eb0cbff5ecdb039ec147d6fbe90fcbd6a9adea87928d87b524aae install-kit-awindows-20260427-211240/ansible/README.md +089595753398c8b82980919d230dafac548c3ba36817f5c96a68051582f9faa3 install-kit-awindows-20260427-211240/ansible/README.md 412bb766bbf0791c3593f38daa771d5d0aa58cc1f2d3c9010fcd4588d0fe87df install-kit-awindows-20260427-211240/ansible/deploy_aw_pfsense_poller.yml 90ac38a33918fcd3620f078f51fbf7c6a9d7f8fd1a34d16b38cb3ac45678b0d7 install-kit-awindows-20260427-211240/ansible/deploy_aw_server.yml -d44adbc4565566039b5fb9dc870b9229c01408208ca8c08082bb5d2c097aa276 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml +a649eeb57472fb259d248983c486f0474bfb7317600feb98f76a80e7af7e4549 install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml 531bfec24f86d28a06e5c0d73005489a818e2c7b1cce1d98f524a3f76802b8ee install-kit-awindows-20260427-211240/ansible/group_vars/all.example.yml 95696c243ab331f06e77a40a9800c4b6668de77675ebbdf2ef54ae49e1b18874 install-kit-awindows-20260427-211240/ansible/group_vars/pfsense-poller.example.yml c5cab36645065815571c99f6d360f910dcccbb54b780c8bfd526a6cdc3684e19 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox-matrix.example.yml 35a33c8a1c75ded5e85c6b79e0b3efde07959ff61ee5f66d83b7e0c2abe87fc5 install-kit-awindows-20260427-211240/ansible/group_vars/proxmox.example.yml -a4333199f454d5c795d3c240ee072342a90d0ed1fba6073be78c3c5e9cb8b32f install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml +7c468f252e328fd3bb7ee776a45feea88efc4b438dfef55b832da4eea867aaf2 install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml 195e7dbdb91f4e77db3263bd0812301ddc912a37ba1519688768bb64a2887567 install-kit-awindows-20260427-211240/ansible/install_full_stack.yml 2e4e94d90143923fefd3ec1257d0ec57daa3e96450d85471bc2c418aae37e105 install-kit-awindows-20260427-211240/ansible/inventory.example.ini d9e43352fd6bdb647db9754ab2c557b6bb27f88f51b2bf23d8c19227e535e7b9 install-kit-awindows-20260427-211240/ansible/provision_proxmox_ct_and_deploy_aw.yml @@ -34,6 +34,7 @@ b497400a1ba57cddf28dc8e217115dc85eccb67150cbdbb6a81abd804ed20109 install-kit-aw 8857f3e17f3f3ed6f211ce7f0a0c46c586a2548541078401ee3befaa20924b3d install-kit-awindows-20260427-211240/windows/dlp-endpoint-signals-collector.ps1 aef0032edd9b1e0c54f7b575664ed511dfc6cb53364e7496cbc95e137678e11a install-kit-awindows-20260427-211240/windows/dlp-policy.example.json f03886caf56c6838e8a163d6b48d1f229e83a5682aeeb447c3a65f13d62dbca4 install-kit-awindows-20260427-211240/windows/hardening-recovery.ps1 +71911cd53ad0abd8bf83994f8a79bbbfe2c0eaf4f4c5f6c2d636dd7786191c2f install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 5dcf249742bd82fa0c803c878bfa0a1344b7b14df85c05f12e8c205663aea158 install-kit-awindows-20260427-211240/windows/validate-deployment.ps1 731098681d89b9af6f3872abd586ac3b1faba2d7f9340211e503f52ad0243b3f install-kit-awindows-20260427-211240/windows/web-category-rules.example.json 41171f0d7ed1e8b00dd0faf1a4b75c9cb063fd09aba8d333851a7a31fb297de1 install-kit-awindows-20260427-211240/windows/worktime-session-collector.ps1 diff --git a/install-kit-awindows-20260427-211240/ansible/README.md b/install-kit-awindows-20260427-211240/ansible/README.md index d642c98..b22fc6c 100644 --- a/install-kit-awindows-20260427-211240/ansible/README.md +++ b/install-kit-awindows-20260427-211240/ansible/README.md @@ -93,6 +93,7 @@ ansible-playbook -i inventory.ini deploy_aw_windows.yml Playbook: - выгружает полный `windows/*` toolkit на целевой хост в InnoSetup-compatible каталог `C:\Program Files\AWatch-rus\windows`, включая DLP и `worktime-session-collector.ps1`; +- если найден legacy config `C:\ProgramData\ActivityWatch-Phase2\deployment-config.json`, выполняет безопасную миграцию через `migrate-awatch-rus-paths.ps1`: backup, остановка задач, перенос данных, переписывание путей, пересоздание scheduled tasks и validation; - выполняет `deploy-ensemble.ps1` (deploy + hardening/recovery) с policy/rules из AWatch-rus toolkit; - после deploy принудительно запускает `ActivityWatch Recovery` и все `ActivityWatch Launch *` задачи; - выполняет API smoke-check bucket `aw-watcher-afk_` и ожидает свежие `not-afk` события; @@ -110,6 +111,9 @@ Playbook: - `aw_windows_install_root: 'C:\Program Files\AWatch-rus\bin'` — каталог бинарников, совпадает с InnoSetup `AwDefaultInstallRoot`; - `aw_windows_state_root: 'C:\ProgramData\AWatch-rus'` — каталог состояния/отчётов, совпадает с InnoSetup `AwDefaultStateRoot`; - `aw_windows_validation_remote_path: '{{ aw_windows_state_root }}\aw_validate_ansible.json'` — отчёт Ansible-валидации хранится рядом с `ensemble-report-*.json`; +- `aw_windows_migration_enabled: true` — включить guard миграции текущего production из `ActivityWatch-Phase2` в единый `AWatch-rus`; +- `aw_windows_legacy_install_root` / `aw_windows_legacy_state_root` — старые production paths, откуда выполняется перенос; +- `aw_windows_migration_report_remote_path` — JSON-отчёт о миграции на Windows-хосте; - `aw_windows_package_version`, `aw_windows_package_url`, `aw_windows_package_zip_path` — версия и источник Windows-пакета ActivityWatch; - `aw_windows_api_smoke_check_bucket: ""` — автоматически использовать `aw-watcher-afk_`; - `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`; diff --git a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml index 41d4657..5d18277 100644 --- a/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml +++ b/install-kit-awindows-20260427-211240/ansible/deploy_aw_windows.yml @@ -42,6 +42,10 @@ aw_windows_api_smoke_check_bucket: "" aw_windows_api_smoke_check_limit: 10 aw_windows_fail_on_validation_error: true + aw_windows_migration_enabled: true + aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2" + aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" + aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json" tasks: - name: Проверить обязательные переменные @@ -74,6 +78,7 @@ - browser-domains-native-collector.ps1 - dlp-endpoint-signals-collector.ps1 - worktime-session-collector.ps1 + - migrate-awatch-rus-paths.ps1 - deploy-domain-users.ps1 - deploy-ensemble.ps1 - hardening-recovery.ps1 @@ -89,6 +94,27 @@ {{ user }} {% endfor -%} + - name: Проверить нужен ли migration с legacy ActivityWatch путей + when: aw_windows_migration_enabled | bool + ansible.windows.win_stat: + path: "{{ aw_windows_legacy_state_root }}\\deployment-config.json" + register: aw_windows_legacy_config + + - name: Выполнить безопасную migration legacy prod в AWatch-rus + when: + - aw_windows_migration_enabled | bool + - aw_windows_legacy_config.stat.exists | default(false) + ansible.windows.win_powershell: + script: | + $ErrorActionPreference = 'Stop' + $result = & "{{ aw_windows_deploy_root }}\windows\migrate-awatch-rus-paths.ps1" ` + -OldInstallRoot "{{ aw_windows_legacy_install_root }}" ` + -OldStateRoot "{{ aw_windows_legacy_state_root }}" ` + -NewInstallRoot "{{ aw_windows_install_root }}" ` + -NewStateRoot "{{ aw_windows_state_root }}" ` + -ToolkitRoot "{{ aw_windows_deploy_root }}\windows" + $result | ConvertTo-Json -Depth 8 | Out-File -FilePath "{{ aw_windows_migration_report_remote_path }}" -Encoding utf8 + - name: Запустить Windows/RDP ensemble развёртывание ansible.windows.win_powershell: script: | diff --git a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml b/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml index ad29b15..30c20c3 100644 --- a/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml +++ b/install-kit-awindows-20260427-211240/ansible/group_vars/windows.example.yml @@ -37,6 +37,12 @@ aw_windows_validation_remote_path: "{{ aw_windows_state_root }}\\aw_validate_ans aw_windows_validation_local_dir: "/tmp/aw-rus-validation" aw_windows_fail_on_validation_error: true +# Безопасная миграция текущего прода со старых путей в единый профиль AWatch-rus. +aw_windows_migration_enabled: true +aw_windows_legacy_install_root: "C:\\Program Files\\ActivityWatch-Phase2" +aw_windows_legacy_state_root: "C:\\ProgramData\\ActivityWatch-Phase2" +aw_windows_migration_report_remote_path: "{{ aw_windows_state_root }}\\aw_migration_ansible.json" + # По умолчанию AFK bucket вычисляется как aw-watcher-afk_. # Задайте явное значение только если watcher пишет в нестандартный bucket. aw_windows_api_smoke_check_enabled: true diff --git a/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 b/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 new file mode 100644 index 0000000..eb7cbe4 --- /dev/null +++ b/install-kit-awindows-20260427-211240/windows/migrate-awatch-rus-paths.ps1 @@ -0,0 +1,214 @@ +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2', + [string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2', + [string]$NewInstallRoot = 'C:\Program Files\AWatch-rus\bin', + [string]$NewStateRoot = 'C:\ProgramData\AWatch-rus', + [string]$ToolkitRoot = 'C:\Program Files\AWatch-rus\windows', + [switch]$SkipValidation +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +Assert-Administrator + +function Copy-DirectoryContents { + param( + [Parameter(Mandatory = $true)] + [string]$Source, + [Parameter(Mandatory = $true)] + [string]$Destination + ) + + if (-not (Test-Path -LiteralPath $Source)) { + return + } + + New-ActivityWatchDirectory -Path $Destination + Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force +} + +function Copy-IfExists { + param( + [Parameter(Mandatory = $true)] + [string]$Source, + [Parameter(Mandatory = $true)] + [string]$Destination + ) + + if (Test-Path -LiteralPath $Source) { + Copy-Item -LiteralPath $Source -Destination $Destination -Force + } +} + +function Convert-PathValue { + param( + [AllowNull()] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return $Value + } + + return $Value.Replace($OldInstallRoot, $NewInstallRoot).Replace($OldStateRoot, $NewStateRoot) +} + +function Stop-AWatchTaskSet { + foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -eq 'ActivityWatch Recovery' -or $_.TaskName -like 'ActivityWatch Launch *' })) { + Stop-ScheduledTask -TaskName $task.TaskName -ErrorAction SilentlyContinue + } +} + +function Get-ExistingAWatchConfig { + $newConfigPath = Join-Path $NewStateRoot 'deployment-config.json' + $oldConfigPath = Join-Path $OldStateRoot 'deployment-config.json' + + if (Test-Path -LiteralPath $oldConfigPath) { + return [pscustomobject]@{ + Path = $oldConfigPath + Config = Read-ActivityWatchDeploymentConfig -Path $oldConfigPath + } + } + + if (Test-Path -LiteralPath $newConfigPath) { + return [pscustomobject]@{ + Path = $newConfigPath + Config = Read-ActivityWatchDeploymentConfig -Path $newConfigPath + } + } + + throw "Не найден deployment-config.json ни в $OldStateRoot, ни в $NewStateRoot." +} + +function Update-AWatchConfigPaths { + param( + [Parameter(Mandatory = $true)] + [pscustomobject]$Config + ) + + $logsRoot = Join-Path $NewStateRoot 'logs' + $Config.paths.installRoot = $NewInstallRoot + $Config.paths.stateRoot = $NewStateRoot + $Config.paths.logsRoot = $logsRoot + $Config.paths.collectorScript = Join-Path $NewStateRoot 'browser-domains-native-collector.ps1' + $Config.paths.endpointCollectorScript = Join-Path $NewStateRoot 'dlp-endpoint-signals-collector.ps1' + if ($Config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { + $Config.paths.sessionCollectorScript = Join-Path $NewStateRoot 'worktime-session-collector.ps1' + } + $Config.paths.rulesPath = Join-Path $NewStateRoot 'web-category-rules.json' + if ($Config.paths.PSObject.Properties.Name -contains 'policyPath') { + $Config.paths.policyPath = Join-Path $NewStateRoot 'dlp-policy.json' + } + $Config.paths.launchScript = Join-Path $NewStateRoot 'launch-watchers.ps1' + $Config.paths.recoveryScript = Join-Path $NewStateRoot 'recovery-loop.ps1' + + if ($Config.PSObject.Properties.Name -contains 'incidentCapture' -and $Config.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { + $Config.incidentCapture.artifactsRoot = Convert-PathValue -Value ([string]$Config.incidentCapture.artifactsRoot) + } + + return $Config +} + +$existing = Get-ExistingAWatchConfig +$backupRoot = Join-Path $NewStateRoot ('migration-backups\' + (Get-Date -Format 'yyyyMMdd-HHmmss')) +$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json' +$newLogsRoot = Join-Path $NewStateRoot 'logs' + +$summary = [ordered]@{ + sourceConfig = $existing.Path + oldInstallRoot = $OldInstallRoot + oldStateRoot = $OldStateRoot + newInstallRoot = $NewInstallRoot + newStateRoot = $NewStateRoot + backupRoot = $backupRoot + actions = @( + 'stop ActivityWatch scheduled tasks', + 'backup old/new install and state directories', + 'copy old install/state contents to AWatch-rus paths', + 'rewrite deployment-config.json paths', + 'regenerate launcher/recovery scripts', + 're-register scheduled tasks', + 'run validate-deployment.ps1' + ) +} + +if ($WhatIfPreference) { + return [pscustomobject]$summary +} + +if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch Windows/RDP путей в AWatch-rus')) { + New-ActivityWatchDirectory -Path $NewStateRoot + New-ActivityWatchDirectory -Path $backupRoot + + Stop-AWatchTaskSet + + foreach ($item in @( + @{ Source = $OldInstallRoot; Name = 'old-install' }, + @{ Source = $OldStateRoot; Name = 'old-state' }, + @{ Source = $NewInstallRoot; Name = 'new-install' }, + @{ Source = $NewStateRoot; Name = 'new-state' } + )) { + if (Test-Path -LiteralPath $item.Source) { + Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force + } + } + + Copy-DirectoryContents -Source $OldInstallRoot -Destination $NewInstallRoot + Copy-DirectoryContents -Source $OldStateRoot -Destination $NewStateRoot + New-ActivityWatchDirectory -Path $newLogsRoot + + foreach ($file in @( + 'browser-domains-native-collector.ps1', + 'dlp-endpoint-signals-collector.ps1', + 'worktime-session-collector.ps1', + 'web-category-rules.example.json', + 'dlp-policy.example.json' + )) { + Copy-IfExists -Source (Join-Path $ToolkitRoot $file) -Destination (Join-Path $NewStateRoot $file) + } + + Copy-IfExists -Source (Join-Path $OldStateRoot 'web-category-rules.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json') + Copy-IfExists -Source (Join-Path $OldStateRoot 'dlp-policy.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json') + if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'web-category-rules.json'))) { + Copy-IfExists -Source (Join-Path $NewStateRoot 'web-category-rules.example.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json') + } + if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'dlp-policy.json'))) { + Copy-IfExists -Source (Join-Path $NewStateRoot 'dlp-policy.example.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json') + } + + $config = Update-AWatchConfigPaths -Config $existing.Config + Write-ActivityWatchDeploymentConfig -Config $config -Path $newConfigPath + Write-ActivityWatchLaunchScript -Path $config.paths.launchScript -ConfigPath $newConfigPath + Write-ActivityWatchRecoveryScript -Path $config.paths.recoveryScript -ConfigPath $newConfigPath + + $taskDefinitions = @($config.userTasks) + Set-ActivityWatchAcl -InstallRoot $NewInstallRoot -StateRoot $NewStateRoot -LogsRoot $newLogsRoot + Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $config.paths.launchScript -ConfigPath $newConfigPath + Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $config.paths.recoveryScript -ConfigPath $newConfigPath + Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName + Start-Sleep -Seconds 5 + + if (-not $SkipValidation) { + $validateScript = Join-Path $ToolkitRoot 'validate-deployment.ps1' + if (-not (Test-Path -LiteralPath $validateScript)) { + $validateScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' + } + $report = & $validateScript -ConfigPath $newConfigPath + if (-not [bool]$report.overallOk) { + throw "Миграция выполнена, но validation завершился ошибкой. Backup: $backupRoot" + } + } + + [pscustomobject]@{ + migrated = $true + backupRoot = $backupRoot + configPath = $newConfigPath + installRoot = $NewInstallRoot + stateRoot = $NewStateRoot + } +} diff --git a/patch.sh b/patch.sh index 002eaea..e8f2c2d 100644 --- a/patch.sh +++ b/patch.sh @@ -38,6 +38,7 @@ cat > windows/installkit/innosetup/innosetup-rdp-package-filelist.md <<'EOF' - `windows/deploy-ensemble.ps1` - `windows/hardening-recovery.ps1` - `windows/validate-deployment.ps1` +- `windows/migrate-awatch-rus-paths.ps1` ### 1.3 Коллекторы - `windows/worktime-session-collector.ps1` (RDP/session presence) @@ -81,6 +82,7 @@ cat > windows/installkit/innosetup/innosetup-rdp-package-filelist.md <<'EOF' - `windows\deploy-ensemble.ps1` - `windows\hardening-recovery.ps1` - `windows\validate-deployment.ps1` +- `windows\migrate-awatch-rus-paths.ps1` - `windows\worktime-session-collector.ps1` - `windows\browser-domains-native-collector.ps1` - `windows\dlp-endpoint-signals-collector.ps1` diff --git a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss index 47ea722..22c0dde 100644 --- a/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss +++ b/windows/installkit/innosetup/AWatch-rus-InnoSetup.iss @@ -38,6 +38,7 @@ Source: "..\..\deploy-domain-users.ps1"; DestDir: "{app}\windows"; Flags: ignore Source: "..\..\deploy-ensemble.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\hardening-recovery.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\validate-deployment.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion +Source: "..\..\migrate-awatch-rus-paths.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\worktime-session-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\browser-domains-native-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion Source: "..\..\dlp-endpoint-signals-collector.ps1"; DestDir: "{app}\windows"; Flags: ignoreversion diff --git a/windows/installkit/innosetup/innosetup-rdp-package-filelist.md b/windows/installkit/innosetup/innosetup-rdp-package-filelist.md index 2a526d3..fba1cc1 100644 --- a/windows/installkit/innosetup/innosetup-rdp-package-filelist.md +++ b/windows/installkit/innosetup/innosetup-rdp-package-filelist.md @@ -31,6 +31,7 @@ - `windows/deploy-ensemble.ps1` - `windows/hardening-recovery.ps1` - `windows/validate-deployment.ps1` +- `windows/migrate-awatch-rus-paths.ps1` ### 1.3 Коллекторы - `windows/worktime-session-collector.ps1` (RDP/session presence) @@ -76,6 +77,7 @@ - `windows\deploy-ensemble.ps1` - `windows\hardening-recovery.ps1` - `windows\validate-deployment.ps1` +- `windows\migrate-awatch-rus-paths.ps1` - `windows\worktime-session-collector.ps1` - `windows\browser-domains-native-collector.ps1` - `windows\dlp-endpoint-signals-collector.ps1` diff --git a/windows/migrate-awatch-rus-paths.ps1 b/windows/migrate-awatch-rus-paths.ps1 new file mode 100644 index 0000000..eb7cbe4 --- /dev/null +++ b/windows/migrate-awatch-rus-paths.ps1 @@ -0,0 +1,214 @@ +[CmdletBinding(SupportsShouldProcess = $true)] +param( + [string]$OldInstallRoot = 'C:\Program Files\ActivityWatch-Phase2', + [string]$OldStateRoot = 'C:\ProgramData\ActivityWatch-Phase2', + [string]$NewInstallRoot = 'C:\Program Files\AWatch-rus\bin', + [string]$NewStateRoot = 'C:\ProgramData\AWatch-rus', + [string]$ToolkitRoot = 'C:\Program Files\AWatch-rus\windows', + [switch]$SkipValidation +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1' +Import-Module $modulePath -Force + +Assert-Administrator + +function Copy-DirectoryContents { + param( + [Parameter(Mandatory = $true)] + [string]$Source, + [Parameter(Mandatory = $true)] + [string]$Destination + ) + + if (-not (Test-Path -LiteralPath $Source)) { + return + } + + New-ActivityWatchDirectory -Path $Destination + Copy-Item -Path (Join-Path $Source '*') -Destination $Destination -Recurse -Force +} + +function Copy-IfExists { + param( + [Parameter(Mandatory = $true)] + [string]$Source, + [Parameter(Mandatory = $true)] + [string]$Destination + ) + + if (Test-Path -LiteralPath $Source) { + Copy-Item -LiteralPath $Source -Destination $Destination -Force + } +} + +function Convert-PathValue { + param( + [AllowNull()] + [string]$Value + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return $Value + } + + return $Value.Replace($OldInstallRoot, $NewInstallRoot).Replace($OldStateRoot, $NewStateRoot) +} + +function Stop-AWatchTaskSet { + foreach ($task in @(Get-ScheduledTask | Where-Object { $_.TaskName -eq 'ActivityWatch Recovery' -or $_.TaskName -like 'ActivityWatch Launch *' })) { + Stop-ScheduledTask -TaskName $task.TaskName -ErrorAction SilentlyContinue + } +} + +function Get-ExistingAWatchConfig { + $newConfigPath = Join-Path $NewStateRoot 'deployment-config.json' + $oldConfigPath = Join-Path $OldStateRoot 'deployment-config.json' + + if (Test-Path -LiteralPath $oldConfigPath) { + return [pscustomobject]@{ + Path = $oldConfigPath + Config = Read-ActivityWatchDeploymentConfig -Path $oldConfigPath + } + } + + if (Test-Path -LiteralPath $newConfigPath) { + return [pscustomobject]@{ + Path = $newConfigPath + Config = Read-ActivityWatchDeploymentConfig -Path $newConfigPath + } + } + + throw "Не найден deployment-config.json ни в $OldStateRoot, ни в $NewStateRoot." +} + +function Update-AWatchConfigPaths { + param( + [Parameter(Mandatory = $true)] + [pscustomobject]$Config + ) + + $logsRoot = Join-Path $NewStateRoot 'logs' + $Config.paths.installRoot = $NewInstallRoot + $Config.paths.stateRoot = $NewStateRoot + $Config.paths.logsRoot = $logsRoot + $Config.paths.collectorScript = Join-Path $NewStateRoot 'browser-domains-native-collector.ps1' + $Config.paths.endpointCollectorScript = Join-Path $NewStateRoot 'dlp-endpoint-signals-collector.ps1' + if ($Config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { + $Config.paths.sessionCollectorScript = Join-Path $NewStateRoot 'worktime-session-collector.ps1' + } + $Config.paths.rulesPath = Join-Path $NewStateRoot 'web-category-rules.json' + if ($Config.paths.PSObject.Properties.Name -contains 'policyPath') { + $Config.paths.policyPath = Join-Path $NewStateRoot 'dlp-policy.json' + } + $Config.paths.launchScript = Join-Path $NewStateRoot 'launch-watchers.ps1' + $Config.paths.recoveryScript = Join-Path $NewStateRoot 'recovery-loop.ps1' + + if ($Config.PSObject.Properties.Name -contains 'incidentCapture' -and $Config.incidentCapture.PSObject.Properties.Name -contains 'artifactsRoot') { + $Config.incidentCapture.artifactsRoot = Convert-PathValue -Value ([string]$Config.incidentCapture.artifactsRoot) + } + + return $Config +} + +$existing = Get-ExistingAWatchConfig +$backupRoot = Join-Path $NewStateRoot ('migration-backups\' + (Get-Date -Format 'yyyyMMdd-HHmmss')) +$newConfigPath = Join-Path $NewStateRoot 'deployment-config.json' +$newLogsRoot = Join-Path $NewStateRoot 'logs' + +$summary = [ordered]@{ + sourceConfig = $existing.Path + oldInstallRoot = $OldInstallRoot + oldStateRoot = $OldStateRoot + newInstallRoot = $NewInstallRoot + newStateRoot = $NewStateRoot + backupRoot = $backupRoot + actions = @( + 'stop ActivityWatch scheduled tasks', + 'backup old/new install and state directories', + 'copy old install/state contents to AWatch-rus paths', + 'rewrite deployment-config.json paths', + 'regenerate launcher/recovery scripts', + 're-register scheduled tasks', + 'run validate-deployment.ps1' + ) +} + +if ($WhatIfPreference) { + return [pscustomobject]$summary +} + +if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Миграция ActivityWatch Windows/RDP путей в AWatch-rus')) { + New-ActivityWatchDirectory -Path $NewStateRoot + New-ActivityWatchDirectory -Path $backupRoot + + Stop-AWatchTaskSet + + foreach ($item in @( + @{ Source = $OldInstallRoot; Name = 'old-install' }, + @{ Source = $OldStateRoot; Name = 'old-state' }, + @{ Source = $NewInstallRoot; Name = 'new-install' }, + @{ Source = $NewStateRoot; Name = 'new-state' } + )) { + if (Test-Path -LiteralPath $item.Source) { + Copy-Item -LiteralPath $item.Source -Destination (Join-Path $backupRoot $item.Name) -Recurse -Force + } + } + + Copy-DirectoryContents -Source $OldInstallRoot -Destination $NewInstallRoot + Copy-DirectoryContents -Source $OldStateRoot -Destination $NewStateRoot + New-ActivityWatchDirectory -Path $newLogsRoot + + foreach ($file in @( + 'browser-domains-native-collector.ps1', + 'dlp-endpoint-signals-collector.ps1', + 'worktime-session-collector.ps1', + 'web-category-rules.example.json', + 'dlp-policy.example.json' + )) { + Copy-IfExists -Source (Join-Path $ToolkitRoot $file) -Destination (Join-Path $NewStateRoot $file) + } + + Copy-IfExists -Source (Join-Path $OldStateRoot 'web-category-rules.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json') + Copy-IfExists -Source (Join-Path $OldStateRoot 'dlp-policy.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json') + if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'web-category-rules.json'))) { + Copy-IfExists -Source (Join-Path $NewStateRoot 'web-category-rules.example.json') -Destination (Join-Path $NewStateRoot 'web-category-rules.json') + } + if (-not (Test-Path -LiteralPath (Join-Path $NewStateRoot 'dlp-policy.json'))) { + Copy-IfExists -Source (Join-Path $NewStateRoot 'dlp-policy.example.json') -Destination (Join-Path $NewStateRoot 'dlp-policy.json') + } + + $config = Update-AWatchConfigPaths -Config $existing.Config + Write-ActivityWatchDeploymentConfig -Config $config -Path $newConfigPath + Write-ActivityWatchLaunchScript -Path $config.paths.launchScript -ConfigPath $newConfigPath + Write-ActivityWatchRecoveryScript -Path $config.paths.recoveryScript -ConfigPath $newConfigPath + + $taskDefinitions = @($config.userTasks) + Set-ActivityWatchAcl -InstallRoot $NewInstallRoot -StateRoot $NewStateRoot -LogsRoot $newLogsRoot + Register-ActivityWatchUserTasks -TaskDefinitions $taskDefinitions -LaunchScriptPath $config.paths.launchScript -ConfigPath $newConfigPath + Register-ActivityWatchRecoveryTask -TaskName $config.recovery.taskName -RecoveryScriptPath $config.paths.recoveryScript -ConfigPath $newConfigPath + Start-ActivityWatchTasks -TaskDefinitions $taskDefinitions -RecoveryTaskName $config.recovery.taskName + Start-Sleep -Seconds 5 + + if (-not $SkipValidation) { + $validateScript = Join-Path $ToolkitRoot 'validate-deployment.ps1' + if (-not (Test-Path -LiteralPath $validateScript)) { + $validateScript = Join-Path $PSScriptRoot 'validate-deployment.ps1' + } + $report = & $validateScript -ConfigPath $newConfigPath + if (-not [bool]$report.overallOk) { + throw "Миграция выполнена, но validation завершился ошибкой. Backup: $backupRoot" + } + } + + [pscustomobject]@{ + migrated = $true + backupRoot = $backupRoot + configPath = $newConfigPath + installRoot = $NewInstallRoot + stateRoot = $NewStateRoot + } +}