diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..417c93e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/.claude/ \ No newline at end of file diff --git a/README.md b/README.md index f795018..0d6ee41 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ Agent działa w oparciu o prosty plik `config.json`, który pozwala łatwo rozsz PSITAgent/ ├── assets.ps1 ├── config.json -└── README.md +├── README.md +└── deploy/ + └── PSITAgent-StartupTask.xml ``` @@ -78,6 +80,17 @@ powershell.exe -ExecutionPolicy Bypass -File .\assets.ps1 ``` +### Alternatywa: import gotowego zadania Harmonogramu Zadań + +Zamiast ręcznie dodawać Startup Script w GPO, można zaimportować gotowy XML z `deploy/PSITAgent-StartupTask.xml` (uruchomienie 5 min po starcie systemu, konto SYSTEM, limit czasu wykonania i automatyczne ponowienie przy błędzie): + +``` +schtasks /Create /TN "PSITAgent" /XML "deploy\PSITAgent-StartupTask.xml" + +``` + +Przed importem podmień w pliku XML ścieżkę do `assets.ps1` na właściwą lokalizację na SYSVOL w swojej domenie. + ## Wymagania - Windows 10 / 11 @@ -95,6 +108,10 @@ powershell.exe -ExecutionPolicy Bypass -File .\assets.ps1 - Skrypt może być dostosowany do pracy offline (lokalna kopia) +- Błędy i przebieg działania są logowane lokalnie do `C:\ProgramData\PSITAgent\assets.log` (log rotowany po przekroczeniu 1 MB) + +- Ostatnio wysłane wartości pól są zapisywane w `C:\ProgramData\PSITAgent\last_sent.json` — skrypt wysyła do Snipe-IT tylko te pola, które faktycznie się zmieniły, ograniczając liczbę wywołań API + ## Licencja diff --git a/assets.ps1 b/assets.ps1 index bf7a42c..4d1661d 100644 --- a/assets.ps1 +++ b/assets.ps1 @@ -2,15 +2,32 @@ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 chcp 65001 | Out-Null +# LOG +$logDir = Join-Path $env:ProgramData "PSITAgent" +$logPath = Join-Path $logDir "assets.log" + +function Write-Log { + param([string]$Message, [string]$Level = "INFO") + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Write-Host $Message + try { + if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } + if ((Test-Path $logPath) -and (Get-Item $logPath).Length -gt 1MB) { + Move-Item -Path $logPath -Destination (Join-Path $logDir "assets.log.bak") -Force + } + Add-Content -Path $logPath -Value "[$timestamp] [$Level] $Message" -Encoding UTF8 + } catch { } +} + # LOAD CONFIG -$configPath = ".\config.json" +$configPath = Join-Path $PSScriptRoot "config.json" $config = Get-Content $configPath -Raw | ConvertFrom-Json $apiUrl = $config."snipe-it".url.TrimEnd('/') $apiKey = $config."snipe-it".apikey $verifySSL = $config."snipe-it"."verify-ssl" -# SSL (optional) +# SSL (opcjonalne - self-signed cert wewnetrznego Snipe-IT) if (-not $verifySSL) { add-type @" using System.Net; @@ -37,21 +54,17 @@ $headers = @{ try { $searchValue = Invoke-Expression $config."search-term".value } catch { - Write-Host "Błąd pobierania search-term" + Write-Log "Blad pobierania wartosci wyszukiwania (search-term)" "ERROR" exit 1 } if (-not $searchValue) { - Write-Host "" - Write-Host "Error getting search value" -ForegroundColor Red - Write-Host "" + Write-Log "Nie udalo sie uzyskac wartosci wyszukiwania" "ERROR" exit 1 } -Write-Host "" -Write-Host "======== Getting Asset ========" -ForegroundColor Cyan -Write-Host "" -Write-Host "Search: $searchValue" +Write-Log "======== Wyszukiwanie assetu ========" +Write-Log "Szukana wartosc: $searchValue" # FIND ASSET $searchUrl = "$apiUrl/hardware?search=$searchValue" @@ -59,21 +72,31 @@ $searchUrl = "$apiUrl/hardware?search=$searchValue" try { $response = Invoke-RestMethod -Uri $searchUrl -Headers $headers -Method GET } catch { - Write-Host "" - Write-Host "Communication error with Snipe-IT API" -ForegroundColor Red - Write-Host "" + Write-Log "Blad komunikacji z API Snipe-IT (wyszukiwanie)" "ERROR" exit 1 } if ($response.total -eq 0) { - Write-Host "" - Write-Host "Nie znaleziono assetu" -ForegroundColor Red - Write-Host "" + Write-Log "Nie znaleziono assetu w Snipe-IT" "ERROR" exit 0 } -$assetId = $response.rows[0].id -Write-Host "Asset ID: $assetId" +# WERYFIKACJA DOPASOWANIA +$searchType = $config."search-term".type +$matched = $response.rows | Where-Object { "$($_.($searchType))".Trim() -eq "$searchValue".Trim() } +$matchCount = ($matched | Measure-Object).Count + +if ($matchCount -eq 0) { + Write-Log "Brak dokladnego dopasowania assetu ($searchType = $searchValue)" "ERROR" + exit 1 +} +if ($matchCount -gt 1) { + Write-Log "Znaleziono wiecej niz jeden pasujacy asset ($searchType = $searchValue) - przerwano aktualizacje" "ERROR" + exit 1 +} + +$assetId = $matched[0].id +Write-Log "ID assetu: $assetId" # COLLECT DATA $customFields = @{} @@ -97,51 +120,68 @@ foreach ($field in $config.fields.custom_fields.PSObject.Properties) { $customFields[$key] = $finalValue } catch { - Write-Host "Błąd pola: $key" + Write-Log "Blad odczytu pola: $key" "ERROR" } } +# MINIMALIZACJA RUCHU API - POROWNANIE Z OSTATNIO WYSLANYMI WARTOSCIAMI +$cachePath = Join-Path $logDir "last_sent.json" +$previous = @{} + +if (Test-Path $cachePath) { + try { + $obj = Get-Content $cachePath -Raw | ConvertFrom-Json + foreach ($p in $obj.PSObject.Properties) { $previous[$p.Name] = $p.Value } + } catch { + $previous = @{} + } +} + +$changedFields = @{} +foreach ($key in $customFields.Keys) { + if (-not $previous.ContainsKey($key) -or "$($previous[$key])" -ne "$($customFields[$key])") { + $changedFields[$key] = $customFields[$key] + } +} + +if ($changedFields.Count -eq 0) { + Write-Log "Brak zmian danych - pominieto aktualizacje API" + exit 0 +} + # PREVIEW -Write-Host "" -Write-Host "=== Data Preview ===" -ForegroundColor Cyan -Write-Host "" -Write-Host "Fields:" -ForegroundColor Yellow +Write-Log "=== Podglad zmienionych danych ===" foreach ($field in $config.fields.custom_fields.PSObject.Properties) { $key = $field.Name $fieldConfig = $field.Value - if (-not $fieldConfig.enabled) { - continue - } - - if ($customFields.ContainsKey($key)) { + if ($changedFields.ContainsKey($key)) { $name = if ($fieldConfig.name) { $fieldConfig.name } else { $key } - $value = $customFields[$key] + $value = $changedFields[$key] $namePadded = $name.PadRight(20) - Write-Host "- $namePadded : $value" + Write-Log "- $namePadded : $value" } } # BUILD BODY -$body = $customFields | ConvertTo-Json -Depth 5 +$body = $changedFields | ConvertTo-Json -Depth 5 # UPDATE ASSET $updateUrl = "$apiUrl/hardware/$assetId" -Write-Host "" -Write-Host "======== Update Asset ========" -ForegroundColor Cyan -Write-Host "" +Write-Log "======== Aktualizacja assetu ========" try { Invoke-RestMethod -Uri $updateUrl -Headers $headers -Method PATCH -Body $body | Out-Null - Write-Host "Snipe-IT Update Completed" -ForegroundColor Green - + Write-Log "Aktualizacja Snipe-IT zakonczona powodzeniem" } catch { - Write-Host "Błąd aktualizacji assetu ID: $assetId" -ForegroundColor Red - Write-Host $_.Exception.Message + Write-Log "Blad aktualizacji assetu ID: $assetId - $($_.Exception.Message)" "ERROR" exit 1 } -Write-Host "" \ No newline at end of file +foreach ($key in $changedFields.Keys) { + $previous[$key] = $changedFields[$key] +} +$previous | ConvertTo-Json -Depth 5 | Set-Content -Path $cachePath -Encoding UTF8 diff --git a/deploy/PSITAgent-StartupTask.xml b/deploy/PSITAgent-StartupTask.xml new file mode 100644 index 0000000..480a874 --- /dev/null +++ b/deploy/PSITAgent-StartupTask.xml @@ -0,0 +1,48 @@ + + + + PSITAgent - wysyla dane sprzetowe do Snipe-IT (uruchamiane 5 min po starcie systemu). + + + + true + PT5M + + + + + S-1-5-18 + HighestAvailable + + + + IgnoreNew + false + false + true + true + false + + false + false + + true + true + false + false + false + PT10M + 7 + + PT5M + 3 + + + + + powershell.exe + + -ExecutionPolicy Bypass -File "\\domain\SysVol\domain\scripts\assets\assets.ps1" + + +