Add site asset CSV import with fuzzy reconciliation
Sites seed inventory from the Asset Collector script or spreadsheet exports, but lookup names drift between sources (Dell vs Dell Inc.), which would otherwise create duplicate vendors/models/types on every import. The importer now normalizes and fuzzy-matches near-miss references onto existing lookup rows within an operator-set similarity threshold, auto-applying the match while reporting it for review so nothing merges silently. Operating systems and asset rows are never reconciled. Adds the dedicated Import Site Assets page with script/template download and a multi-file dry-run/commit workflow.
This commit is contained in:
542
scripts/site_imports/assetcollector/Collect-ShopDB.ps1
Normal file
542
scripts/site_imports/assetcollector/Collect-ShopDB.ps1
Normal file
@@ -0,0 +1,542 @@
|
||||
#Requires -Version 5.1
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# Where CSVs are collected/accumulated. Overrides the in-script CONFIG block
|
||||
# below. See the CONFIG block for the easy place to set this permanently.
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$OutputRoot,
|
||||
|
||||
# Optional network share to mirror the store into after a run. Overrides the
|
||||
# in-script CONFIG block below.
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$PublishRoot,
|
||||
|
||||
# Business identifier for this PC (workstation number). Defaults to hostname.
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$AssetNumber,
|
||||
|
||||
# Value written into assets.assettypeid for the PC (resolved by name).
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$AssetType = 'Computer',
|
||||
|
||||
# Value written into computers.computertypeid (resolved by name).
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$ComputerType,
|
||||
|
||||
# Value written into machines.machinetypeid (resolved by name).
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$MachineType,
|
||||
|
||||
# Explicit eDNC/UDC source overrides. When omitted, standard on-asset
|
||||
# locations are searched.
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$RegPath,
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$JsonPath,
|
||||
|
||||
# Skip switches for partial runs.
|
||||
[switch]$SkipMachine,
|
||||
[switch]$SkipApps
|
||||
)
|
||||
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Unified ShopDB site-adoption collector. Runs on any PC, collects the PC and any
|
||||
attached eDNC/DNC machine, and UPSERTS the results into a shared set of
|
||||
importable CSVs so a site can build its inventory PC by PC.
|
||||
|
||||
.DESCRIPTION
|
||||
Accumulating store under OutputRoot:
|
||||
vendors.csv, models.csv, operatingsystems.csv, assets.csv,
|
||||
computers.csv, machines.csv
|
||||
Rows are merged field-by-field on each table's natural key (a later run fills
|
||||
gaps and refreshes values without wiping data an earlier run captured).
|
||||
|
||||
Phase-2 data that CSV cannot carry is written per entity under OutputRoot\sidecars:
|
||||
computer_<assetnumber>.json (interfaces, installed apps)
|
||||
machine_<machinenumber>.json (PC<->Machine link, connection detail)
|
||||
|
||||
Read-only against the machine. Local reads may run elevated; publishing to a
|
||||
share runs in the current user context only.
|
||||
|
||||
.NOTES
|
||||
PS 5.1 compatible. Headers match the ShopDB importer (see CSV-SCHEMA.md).
|
||||
#>
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# =============================================================================
|
||||
# CONFIG - SET YOUR CSV DESTINATIONS HERE
|
||||
# -----------------------------------------------------------------------------
|
||||
# Two-stage flow so you can collect locally (possibly elevated) and then append
|
||||
# the captured rows to a CSV store that lives on a network share:
|
||||
#
|
||||
# 1) LocalStageRoot Fast, always-writable local folder where CSVs are built
|
||||
# and merged. Collection can run elevated here.
|
||||
# 2) NetworkStoreRoot The shared CSV store the whole site accumulates into and
|
||||
# that you later point 'flask csv import --dir' at. Leave
|
||||
# blank ('') to keep everything local. When set, the merged
|
||||
# store is copied here at the end of the run. Network
|
||||
# writes must happen as a STANDARD user - do NOT run the
|
||||
# publish step elevated.
|
||||
#
|
||||
# Example network path: '\\fileserver\shopdb$\intake'
|
||||
#
|
||||
# Command-line -OutputRoot / -PublishRoot still override these values.
|
||||
# =============================================================================
|
||||
|
||||
$LocalStageRoot = Join-Path $env:ProgramData 'ShopDB\intake'
|
||||
$NetworkStoreRoot = '\\wi001dataaege.wlm.geaerospace.net\shared\TEAMSYS\.sfscripts\shopdb'
|
||||
|
||||
# Resolve effective destinations (command-line params win over CONFIG above).
|
||||
if (-not $OutputRoot) { $OutputRoot = $LocalStageRoot }
|
||||
if (-not $PublishRoot) { $PublishRoot = $NetworkStoreRoot }
|
||||
|
||||
# --- Store definitions (header order + natural key per importable table) -------
|
||||
|
||||
$script:Stores = [ordered]@{
|
||||
'vendors.csv' = @{ key = 'vendor'; header = @('vendor') }
|
||||
'models.csv' = @{ key = 'modelnumber'; header = @('modelnumber','vendorid','modeltypeid') }
|
||||
'operatingsystems.csv' = @{ key = 'osname'; header = @('osname') }
|
||||
'assets.csv' = @{ key = 'assetnumber'; header = @('assetnumber','assettypeid','name','statusid','serialnumber','gaugelabreference','maintenancereference','locationid','businessunitid','notes') }
|
||||
'computers.csv' = @{ key = 'assetid'; header = @('assetid','hostname','computertypeid','osid','vendorid','modelnumberid','loggedinuser','lastreporteddate','lastboottime') }
|
||||
'machines.csv' = @{ key = 'assetid'; header = @('assetid','machinetypeid','vendorid','modelnumberid','controllervendorid','controllermodelid','requiresmanualconfig','islocationonly','notes') }
|
||||
}
|
||||
|
||||
# --- Shared helpers -----------------------------------------------------------
|
||||
|
||||
function ConvertTo-CsvField {
|
||||
param([AllowNull()]$Value)
|
||||
if ($null -eq $Value) { return '' }
|
||||
$text = [string]$Value
|
||||
if ($text -match '[",\r\n]') { return '"' + ($text -replace '"', '""') + '"' }
|
||||
return $text
|
||||
}
|
||||
|
||||
function Get-Prop {
|
||||
param([AllowNull()]$Object, [Parameter(Mandatory = $true)][string]$Name)
|
||||
if ($null -eq $Object) { return $null }
|
||||
$prop = $Object.PSObject.Properties[$Name]
|
||||
if ($null -eq $prop) { return $null }
|
||||
return $prop.Value
|
||||
}
|
||||
|
||||
function Get-NonEmpty {
|
||||
param([AllowNull()]$Value)
|
||||
if ($null -ne $Value -and -not [string]::IsNullOrWhiteSpace([string]$Value)) {
|
||||
return ([string]$Value).Trim()
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-FirstNonEmpty {
|
||||
param([object[]]$Values)
|
||||
foreach ($v in $Values) { $n = Get-NonEmpty $v; if ($n) { return $n } }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Format-ShopDbDate {
|
||||
param([AllowNull()]$Value)
|
||||
if ($null -eq $Value) { return $null }
|
||||
try { return ([datetime]$Value).ToString('yyyy-MM-dd HH:mm:ss') } catch { return $null }
|
||||
}
|
||||
|
||||
function Write-Utf8NoBom {
|
||||
param([string]$Path, [string]$Content)
|
||||
$encoding = New-Object System.Text.UTF8Encoding($false)
|
||||
[System.IO.File]::WriteAllText($Path, $Content, $encoding)
|
||||
}
|
||||
|
||||
# Field-level upsert into a persistent CSV store, keyed by natural key. Existing
|
||||
# values are kept when the incoming cell is empty, so a partial run never blanks
|
||||
# out data an earlier run captured.
|
||||
function Merge-CsvStore {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Path,
|
||||
[Parameter(Mandatory = $true)][string[]]$Header,
|
||||
[Parameter(Mandatory = $true)][string]$Key,
|
||||
[Parameter(Mandatory = $true)][AllowNull()][object[]]$IncomingRows
|
||||
)
|
||||
|
||||
$index = [ordered]@{}
|
||||
if (Test-Path -LiteralPath $Path) {
|
||||
foreach ($existing in @(Import-Csv -LiteralPath $Path)) {
|
||||
$keyVal = Get-NonEmpty (Get-Prop $existing $Key)
|
||||
if (-not $keyVal) { continue }
|
||||
$index[$keyVal.ToLowerInvariant()] = $existing
|
||||
}
|
||||
}
|
||||
|
||||
$changed = 0
|
||||
foreach ($row in $IncomingRows) {
|
||||
if ($null -eq $row) { continue }
|
||||
$keyVal = Get-NonEmpty (Get-Prop $row $Key)
|
||||
if (-not $keyVal) { continue }
|
||||
$lc = $keyVal.ToLowerInvariant()
|
||||
if ($index.Contains($lc)) {
|
||||
$target = $index[$lc]
|
||||
foreach ($col in $Header) {
|
||||
$incoming = Get-NonEmpty (Get-Prop $row $col)
|
||||
if ($incoming) {
|
||||
if ($target.PSObject.Properties[$col]) { $target.$col = $incoming }
|
||||
else { $target | Add-Member -NotePropertyName $col -NotePropertyValue $incoming -Force }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$fresh = [ordered]@{}
|
||||
foreach ($col in $Header) { $fresh[$col] = Get-NonEmpty (Get-Prop $row $col) }
|
||||
$index[$lc] = [pscustomobject]$fresh
|
||||
}
|
||||
$changed++
|
||||
}
|
||||
|
||||
$lines = New-Object System.Collections.Generic.List[string]
|
||||
$lines.Add(($Header -join ','))
|
||||
foreach ($stored in $index.Values) {
|
||||
$cells = foreach ($col in $Header) { ConvertTo-CsvField (Get-Prop $stored $col) }
|
||||
$lines.Add(($cells -join ','))
|
||||
}
|
||||
Write-Utf8NoBom -Path $Path -Content (($lines -join "`r`n") + "`r`n")
|
||||
return $changed
|
||||
}
|
||||
|
||||
# --- eDNC/UDC parsing (registry, .reg export, UDC settings json) --------------
|
||||
|
||||
function Resolve-ControllerVendor {
|
||||
param([AllowNull()][string]$ControlType, [AllowNull()][string]$NcIF)
|
||||
$ct = ('' + $ControlType).Trim().ToUpperInvariant()
|
||||
$nc = ('' + $NcIF).Trim().ToUpperInvariant()
|
||||
if ($nc -eq 'EFOCAS' -or $ct -eq 'EFOCAS') { return 'FANUC' }
|
||||
if ($ct -eq 'SIEMENS') { return 'Siemens' }
|
||||
if ($ct -eq 'OKUMA') { return 'Okuma' }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Read-RegExport {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
$lines = @(Get-Content -LiteralPath $Path -Encoding Unicode -ErrorAction Stop)
|
||||
$firstMeaningful = ($lines | Where-Object { $_ -match '\S' } | Select-Object -First 1)
|
||||
if (-not ($firstMeaningful -match '^(REGEDIT4|Windows Registry Editor Version)')) {
|
||||
$lines = @(Get-Content -LiteralPath $Path -Encoding Default -ErrorAction Stop)
|
||||
}
|
||||
$data = @{}
|
||||
$currentKey = $null
|
||||
foreach ($raw in $lines) {
|
||||
$line = $raw.Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { continue }
|
||||
if ($line -match '^(REGEDIT4|Windows Registry Editor Version)') { continue }
|
||||
if ($line -match '^\s*;') { continue }
|
||||
if ($line -match '^\[(?<key>[^\]]+)\]$') {
|
||||
$currentKey = $Matches['key']
|
||||
if (-not $data.ContainsKey($currentKey)) { $data[$currentKey] = @{} }
|
||||
continue
|
||||
}
|
||||
if (-not $currentKey) { continue }
|
||||
if ($line -match '^"(?<name>[^"]+)"=(?<rhs>.+)$') {
|
||||
$name = $Matches['name']
|
||||
$rhs = $Matches['rhs'].Trim()
|
||||
if ($rhs -match '^"(?<s>.*)"$') { $val = $Matches['s'] }
|
||||
elseif ($rhs -match '^dword:(?<hex>[0-9a-fA-F]{8})$') { $val = [Convert]::ToInt32($Matches['hex'], 16) }
|
||||
else { $val = $rhs }
|
||||
$data[$currentKey][$name] = $val
|
||||
}
|
||||
}
|
||||
return $data
|
||||
}
|
||||
|
||||
function Get-RegExportValue {
|
||||
param([hashtable]$Reg, [string]$KeySuffix, [string]$Name)
|
||||
foreach ($key in $Reg.Keys) {
|
||||
if ($key -match ('\\' + [regex]::Escape($KeySuffix) + '$')) {
|
||||
if ($Reg[$key].ContainsKey($Name)) { return $Reg[$key][$Name] }
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function ConvertFrom-RegExport {
|
||||
param([hashtable]$Reg)
|
||||
[ordered]@{
|
||||
source = 'reg'
|
||||
machinenumber = Get-NonEmpty (Get-RegExportValue $Reg 'General' 'MachineNo')
|
||||
site = Get-NonEmpty (Get-RegExportValue $Reg 'General' 'Site')
|
||||
controltype = Get-NonEmpty (Get-RegExportValue $Reg 'General' 'Cnc')
|
||||
ncif = Get-NonEmpty (Get-RegExportValue $Reg 'General' 'NcIF')
|
||||
dnctype = 'eDNC'
|
||||
tcpaddress = Get-NonEmpty (Get-RegExportValue $Reg 'eFocas' 'IpAddr')
|
||||
tcpport = Get-NonEmpty (Get-RegExportValue $Reg 'eFocas' 'SocketNo')
|
||||
comport = Get-NonEmpty (Get-RegExportValue $Reg 'PPDCS' 'Port Id')
|
||||
baudrate = Get-NonEmpty (Get-RegExportValue $Reg 'PPDCS' 'Baud')
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RegistryValue {
|
||||
param([string]$Path, [string]$Name)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return $null }
|
||||
return Get-Prop (Get-ItemProperty -LiteralPath $Path -ErrorAction SilentlyContinue) $Name
|
||||
}
|
||||
|
||||
function ConvertFrom-LiveRegistry {
|
||||
param([string]$Base)
|
||||
if (-not (Test-Path -LiteralPath $Base)) { return $null }
|
||||
$general = Join-Path $Base 'General'
|
||||
$efocas = Join-Path $Base 'eFocas'
|
||||
$ppdcs = Join-Path $Base 'PPDCS'
|
||||
[ordered]@{
|
||||
source = 'registry'
|
||||
machinenumber = Get-NonEmpty (Get-RegistryValue $general 'MachineNo')
|
||||
site = Get-NonEmpty (Get-RegistryValue $general 'Site')
|
||||
controltype = Get-NonEmpty (Get-RegistryValue $general 'Cnc')
|
||||
ncif = Get-NonEmpty (Get-RegistryValue $general 'NcIF')
|
||||
dnctype = 'eDNC'
|
||||
tcpaddress = Get-NonEmpty (Get-RegistryValue $efocas 'IpAddr')
|
||||
tcpport = Get-NonEmpty (Get-RegistryValue $efocas 'SocketNo')
|
||||
comport = Get-NonEmpty (Get-RegistryValue $ppdcs 'Port Id')
|
||||
baudrate = Get-NonEmpty (Get-RegistryValue $ppdcs 'Baud')
|
||||
}
|
||||
}
|
||||
|
||||
function ConvertFrom-UdcJson {
|
||||
param([string]$Path)
|
||||
$obj = Get-Content -LiteralPath $Path -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
$general = Get-Prop $obj 'GeneralSettings'
|
||||
$ethernet = Get-Prop $obj 'EthernetSettings'
|
||||
$serial = Get-Prop $obj 'SerialSettings'
|
||||
[ordered]@{
|
||||
source = 'json'
|
||||
machinenumber = Get-NonEmpty (Get-Prop $general 'MachineNumber')
|
||||
site = Get-NonEmpty (Get-Prop $general 'Site')
|
||||
controltype = Get-NonEmpty (Get-Prop $general 'ControlType')
|
||||
ncif = $null
|
||||
dnctype = Get-NonEmpty (Get-Prop $general 'DncType')
|
||||
tcpaddress = Get-NonEmpty (Get-Prop $ethernet 'TCPAddress')
|
||||
tcpport = Get-NonEmpty (Get-Prop $ethernet 'TCPPort')
|
||||
comport = Get-NonEmpty (Get-Prop $serial 'ComPort')
|
||||
baudrate = Get-NonEmpty (Get-Prop $serial 'BaudRate')
|
||||
}
|
||||
}
|
||||
|
||||
function Find-FirstFile {
|
||||
param([string[]]$Roots, [string]$Filter)
|
||||
foreach ($root in $Roots) {
|
||||
if (-not $root -or -not (Test-Path -LiteralPath $root)) { continue }
|
||||
$hit = Get-ChildItem -LiteralPath $root -Filter $Filter -File -ErrorAction SilentlyContinue |
|
||||
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($hit) { return $hit.FullName }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-DncSource {
|
||||
param([string]$RegPath, [string]$JsonPath)
|
||||
if ($JsonPath -and (Test-Path -LiteralPath $JsonPath)) {
|
||||
Write-Host (' machine source: JSON (explicit) ' + $JsonPath) -ForegroundColor DarkGray
|
||||
return ConvertFrom-UdcJson -Path $JsonPath
|
||||
}
|
||||
if ($RegPath -and (Test-Path -LiteralPath $RegPath)) {
|
||||
Write-Host (' machine source: .reg (explicit) ' + $RegPath) -ForegroundColor DarkGray
|
||||
return ConvertFrom-RegExport -Reg (Read-RegExport -Path $RegPath)
|
||||
}
|
||||
foreach ($base in @(
|
||||
'HKLM:\SOFTWARE\GE Aircraft Engines\DNC',
|
||||
'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC')) {
|
||||
$fromReg = ConvertFrom-LiveRegistry -Base $base
|
||||
if ($fromReg -and $fromReg.machinenumber) {
|
||||
Write-Host (' machine source: live registry ' + $base) -ForegroundColor DarkGray
|
||||
return $fromReg
|
||||
}
|
||||
}
|
||||
$localReg = Find-FirstFile -Roots @('C:\Program Files (x86)\Dnc', (Join-Path $env:SystemDrive 'GE Aerospace')) -Filter '*.reg'
|
||||
if ($localReg) {
|
||||
Write-Host (' machine source: local .reg ' + $localReg) -ForegroundColor DarkGray
|
||||
return ConvertFrom-RegExport -Reg (Read-RegExport -Path $localReg)
|
||||
}
|
||||
$localJson = Find-FirstFile -Roots @((Join-Path $env:SystemDrive 'GE Aerospace'), 'C:\Program Files (x86)\UDC', 'C:\Program Files (x86)\Dnc') -Filter 'udc_settings_*.json'
|
||||
if ($localJson) {
|
||||
Write-Host (' machine source: local UDC json ' + $localJson) -ForegroundColor DarkGray
|
||||
return ConvertFrom-UdcJson -Path $localJson
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- PC collection (read-only) ------------------------------------------------
|
||||
|
||||
function Get-PcFacts {
|
||||
$computerSystem = Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue
|
||||
$operatingSys = Get-CimInstance Win32_OperatingSystem -ErrorAction SilentlyContinue
|
||||
$bios = Get-CimInstance Win32_BIOS -ErrorAction SilentlyContinue
|
||||
|
||||
$interfaces = @()
|
||||
try {
|
||||
foreach ($cfg in @(Get-NetIPConfiguration -ErrorAction SilentlyContinue)) {
|
||||
$adapter = $null
|
||||
try { $adapter = Get-NetAdapter -InterfaceAlias $cfg.InterfaceAlias -ErrorAction SilentlyContinue } catch {}
|
||||
if (-not $adapter -or (Get-Prop $adapter 'HardwareInterface') -ne $true) { continue }
|
||||
$ipv4 = @(Get-Prop $cfg 'IPv4Address') | Select-Object -First 1
|
||||
$interfaces += [ordered]@{
|
||||
interfacealias = [string]$cfg.InterfaceAlias
|
||||
ipaddress = if ($ipv4) { [string](Get-Prop $ipv4 'IPAddress') } else { $null }
|
||||
macaddress = [string](Get-Prop $adapter 'MacAddress')
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
|
||||
$installedApps = @()
|
||||
if (-not $SkipApps) {
|
||||
foreach ($keyPath in @(
|
||||
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
|
||||
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*')) {
|
||||
foreach ($entry in @(Get-ItemProperty -Path $keyPath -ErrorAction SilentlyContinue)) {
|
||||
$displayName = Get-NonEmpty (Get-Prop $entry 'DisplayName')
|
||||
if (-not $displayName) { continue }
|
||||
if ((Get-Prop $entry 'SystemComponent') -eq 1) { continue }
|
||||
$installedApps += [ordered]@{
|
||||
appname = $displayName
|
||||
installedversion = Get-NonEmpty (Get-Prop $entry 'DisplayVersion')
|
||||
publisher = Get-NonEmpty (Get-Prop $entry 'Publisher')
|
||||
}
|
||||
}
|
||||
}
|
||||
$installedApps = @($installedApps | Sort-Object { $_.appname } -Unique)
|
||||
}
|
||||
|
||||
[ordered]@{
|
||||
hostname = Get-FirstNonEmpty @($env:COMPUTERNAME, (Get-Prop $computerSystem 'Name'))
|
||||
vendor = Get-NonEmpty (Get-Prop $computerSystem 'Manufacturer')
|
||||
model = Get-NonEmpty (Get-Prop $computerSystem 'Model')
|
||||
osname = Get-NonEmpty (Get-Prop $operatingSys 'Caption')
|
||||
serial = Get-FirstNonEmpty @((Get-Prop $bios 'SerialNumber'), (Get-Prop $computerSystem 'SerialNumber'))
|
||||
lastboot = Format-ShopDbDate (Get-Prop $operatingSys 'LastBootUpTime')
|
||||
loggeduser = Get-NonEmpty (Get-Prop $computerSystem 'UserName')
|
||||
interfaces = $interfaces
|
||||
installedapps = $installedApps
|
||||
}
|
||||
}
|
||||
|
||||
# --- Main ---------------------------------------------------------------------
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' ShopDB unified collector' -ForegroundColor Cyan
|
||||
Write-Host (' ' + ('-' * 56)) -ForegroundColor DarkGray
|
||||
|
||||
if (-not (Test-Path -LiteralPath $OutputRoot)) { New-Item -ItemType Directory -Path $OutputRoot -Force | Out-Null }
|
||||
$sidecarDir = Join-Path $OutputRoot 'sidecars'
|
||||
if (-not (Test-Path -LiteralPath $sidecarDir)) { New-Item -ItemType Directory -Path $sidecarDir -Force | Out-Null }
|
||||
|
||||
$collectedAt = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
|
||||
|
||||
$pc = Get-PcFacts
|
||||
if (-not $AssetNumber) { $AssetNumber = $pc.hostname }
|
||||
|
||||
# Row sets keyed to the shared stores.
|
||||
$vendorRows = @()
|
||||
$modelRows = @()
|
||||
$osRows = @()
|
||||
$assetRows = @()
|
||||
$computerRows = @()
|
||||
$machineRows = @()
|
||||
|
||||
if ($pc.vendor) { $vendorRows += [pscustomobject]@{ vendor = $pc.vendor } }
|
||||
if ($pc.model) { $modelRows += [pscustomobject]@{ modelnumber = $pc.model; vendorid = $pc.vendor; modeltypeid = $null } }
|
||||
if ($pc.osname) { $osRows += [pscustomobject]@{ osname = $pc.osname } }
|
||||
|
||||
$assetRows += [pscustomobject]@{
|
||||
assetnumber = $AssetNumber; assettypeid = $AssetType; name = $pc.hostname; statusid = $null
|
||||
serialnumber = $pc.serial; gaugelabreference = $null; maintenancereference = $null
|
||||
locationid = $null; businessunitid = $null
|
||||
notes = 'PC collected ' + $collectedAt
|
||||
}
|
||||
$computerRows += [pscustomobject]@{
|
||||
assetid = $AssetNumber; hostname = $pc.hostname; computertypeid = $ComputerType; osid = $pc.osname
|
||||
vendorid = $pc.vendor; modelnumberid = $pc.model; loggedinuser = $pc.loggeduser
|
||||
lastreporteddate = $collectedAt; lastboottime = $pc.lastboot
|
||||
}
|
||||
|
||||
# Machine (best-effort).
|
||||
$machine = $null
|
||||
if (-not $SkipMachine) {
|
||||
try { $machine = Resolve-DncSource -RegPath $RegPath -JsonPath $JsonPath } catch { $machine = $null }
|
||||
}
|
||||
if ($machine -and $machine.machinenumber) {
|
||||
$controllerVendor = Resolve-ControllerVendor -ControlType $machine.controltype -NcIF $machine.ncif
|
||||
$transport = if ($machine.tcpaddress) { 'ethernet' } elseif ($machine.comport) { 'serial' } else { 'unknown' }
|
||||
$noteBits = @("DNC=$($machine.dnctype)")
|
||||
if ($machine.controltype) { $noteBits += "control=$($machine.controltype)" }
|
||||
$noteBits += "transport=$transport"
|
||||
$machineNote = 'eDNC collected ' + $collectedAt + '; ' + ($noteBits -join '; ')
|
||||
|
||||
if ($controllerVendor) { $vendorRows += [pscustomobject]@{ vendor = $controllerVendor } }
|
||||
$assetRows += [pscustomobject]@{
|
||||
assetnumber = $machine.machinenumber; assettypeid = 'Machine'; name = $machine.machinenumber; statusid = $null
|
||||
serialnumber = $null; gaugelabreference = $null; maintenancereference = $null
|
||||
locationid = $null; businessunitid = $null; notes = $machineNote
|
||||
}
|
||||
$machineRows += [pscustomobject]@{
|
||||
assetid = $machine.machinenumber; machinetypeid = $MachineType; vendorid = $null; modelnumberid = $null
|
||||
controllervendorid = $controllerVendor; controllermodelid = $null
|
||||
requiresmanualconfig = $null; islocationonly = $null; notes = $machineNote
|
||||
}
|
||||
|
||||
$link = [ordered]@{
|
||||
pcassetnumber = $AssetNumber; machineassetnumber = $machine.machinenumber; relationship = 'controls'
|
||||
dnctype = $machine.dnctype; controltype = $machine.controltype; controllervendor = $controllerVendor
|
||||
site = $machine.site; source = $machine.source; collectedat = $collectedAt
|
||||
connection = [ordered]@{
|
||||
transport = $transport; tcpaddress = $machine.tcpaddress; tcpport = $machine.tcpport
|
||||
comport = $machine.comport; baudrate = $machine.baudrate
|
||||
}
|
||||
}
|
||||
Write-Utf8NoBom -Path (Join-Path $sidecarDir ('machine_' + $machine.machinenumber + '.json')) -Content ($link | ConvertTo-Json -Depth 6)
|
||||
Write-Host (' linked machine ' + $machine.machinenumber + ' (' + $controllerVendor + ', ' + $transport + ')') -ForegroundColor DarkGray
|
||||
} elseif (-not $SkipMachine) {
|
||||
Write-Host ' no eDNC/DNC/UDC machine config found on this PC' -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# --- Upsert into the shared store --------------------------------------------
|
||||
|
||||
$rowsByStore = @{
|
||||
'vendors.csv' = $vendorRows
|
||||
'models.csv' = $modelRows
|
||||
'operatingsystems.csv' = $osRows
|
||||
'assets.csv' = $assetRows
|
||||
'computers.csv' = $computerRows
|
||||
'machines.csv' = $machineRows
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host (' Store: ' + $OutputRoot) -ForegroundColor Green
|
||||
foreach ($fileName in $script:Stores.Keys) {
|
||||
$spec = $script:Stores[$fileName]
|
||||
$rows = @($rowsByStore[$fileName])
|
||||
if ($rows.Count -eq 0) { continue }
|
||||
$path = Join-Path $OutputRoot $fileName
|
||||
$n = Merge-CsvStore -Path $path -Header $spec.header -Key $spec.key -IncomingRows $rows
|
||||
Write-Host (' merged ' + $n + ' row(s) into ' + $fileName) -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
# PC phase-2 sidecar.
|
||||
$pcSidecar = [ordered]@{
|
||||
assetnumber = $AssetNumber; hostname = $pc.hostname; collectedat = $collectedAt
|
||||
interfaces = $pc.interfaces; installedapps = $pc.installedapps
|
||||
}
|
||||
Write-Utf8NoBom -Path (Join-Path $sidecarDir ('computer_' + $AssetNumber + '.json')) -Content ($pcSidecar | ConvertTo-Json -Depth 6)
|
||||
|
||||
# --- Publish (standard-user network mirror, optional) -------------------------
|
||||
|
||||
if ($PublishRoot) {
|
||||
if (([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
|
||||
[Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||
Write-Host ''
|
||||
Write-Host ' WARNING: publishing while elevated. Network writes should run as the standard user.' -ForegroundColor Yellow
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $PublishRoot)) { New-Item -ItemType Directory -Path $PublishRoot -Force | Out-Null }
|
||||
Copy-Item -Path (Join-Path $OutputRoot '*') -Destination $PublishRoot -Recurse -Force
|
||||
Write-Host ''
|
||||
Write-Host (' Published store to: ' + $PublishRoot) -ForegroundColor Green
|
||||
}
|
||||
|
||||
Write-Host ''
|
||||
Write-Host ' Done. Dry-run validate the accumulated store with:' -ForegroundColor Cyan
|
||||
Write-Host (' flask csv import --dir "' + $OutputRoot + '"') -ForegroundColor White
|
||||
Write-Host ''
|
||||
Reference in New Issue
Block a user