v1.0.0
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/.claude/
|
||||
19
README.md
19
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
|
||||
|
||||
|
||||
118
assets.ps1
118
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 ""
|
||||
foreach ($key in $changedFields.Keys) {
|
||||
$previous[$key] = $changedFields[$key]
|
||||
}
|
||||
$previous | ConvertTo-Json -Depth 5 | Set-Content -Path $cachePath -Encoding UTF8
|
||||
|
||||
48
deploy/PSITAgent-StartupTask.xml
Normal file
48
deploy/PSITAgent-StartupTask.xml
Normal file
@@ -0,0 +1,48 @@
|
||||
<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.3" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Description>PSITAgent - wysyla dane sprzetowe do Snipe-IT (uruchamiane 5 min po starcie systemu).</Description>
|
||||
</RegistrationInfo>
|
||||
<Triggers>
|
||||
<BootTrigger>
|
||||
<Enabled>true</Enabled>
|
||||
<Delay>PT5M</Delay>
|
||||
</BootTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<UserId>S-1-5-18</UserId>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>true</AllowHardTerminate>
|
||||
<StartWhenAvailable>true</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT10M</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
<RestartOnFailure>
|
||||
<Interval>PT5M</Interval>
|
||||
<Count>3</Count>
|
||||
</RestartOnFailure>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>powershell.exe</Command>
|
||||
<!-- Podmien scieżkę ponizej na wlasciwa domene / lokalizacje na SYSVOL -->
|
||||
<Arguments>-ExecutionPolicy Bypass -File "\\domain\SysVol\domain\scripts\assets\assets.ps1"</Arguments>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>
|
||||
Reference in New Issue
Block a user