|
|
|
|
@@ -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 ''
|