Добавить безопасную миграцию prod в AWatch-rus

This commit is contained in:
Devin AI
2026-05-02 11:58:28 +00:00
parent 2ec90b05ae
commit 2e49310023
14 changed files with 553 additions and 6 deletions
+4
View File
@@ -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_<COMPUTERNAME>` и ожидает свежие `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_<COMPUTERNAME>`;
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
+26
View File
@@ -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: |
+6
View File
@@ -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_<COMPUTERNAME>.
# Задайте явное значение только если watcher пишет в нестандартный bucket.
aw_windows_api_smoke_check_enabled: true
+13
View File
@@ -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
+31 -3
View File
@@ -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` те же файлы формируются в указанном каталоге.
## Повторный прогон
@@ -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
@@ -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_<COMPUTERNAME>` и ожидает свежие `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_<COMPUTERNAME>`;
- `aw_windows_fail_on_validation_error: true` — завершать playbook ошибкой, если `validate-deployment.ps1` возвращает `overallOk=false`;
@@ -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: |
@@ -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_<COMPUTERNAME>.
# Задайте явное значение только если watcher пишет в нестандартный bucket.
aw_windows_api_smoke_check_enabled: true
@@ -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
}
}
+2
View File
@@ -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`
@@ -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
@@ -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`
+214
View File
@@ -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
}
}