Merge remote-tracking branch 'origin/wj' into wilm
This commit is contained in:
264
plugins/computers/client/Set-ShopDBDeviceId.ps1
Normal file
264
plugins/computers/client/Set-ShopDBDeviceId.ps1
Normal file
@@ -0,0 +1,264 @@
|
||||
#requires -Version 5.1
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Seed HKLM:\SOFTWARE\GE\ShopDB\DeviceId from whatever identity a bay already
|
||||
carries, so device identity can move off C:\Enrollment and into the
|
||||
registry.
|
||||
|
||||
.DESCRIPTION
|
||||
Phase 3 of docs/proposals/device-identity.md. The registry is the
|
||||
destination because it survives the profile cleanup and disk hygiene that
|
||||
eats C:\Enrollment, is readable as SYSTEM with no file ACL to get wrong, and
|
||||
is already where the collector looks for BaseUrl and ApiToken.
|
||||
|
||||
This script does the one-time backfill: it reads the identity a bay holds
|
||||
today and writes it to DeviceId. It is idempotent and safe to run every
|
||||
GE-Enforce cycle.
|
||||
|
||||
THE SOURCE DEPENDS ON THE PC TYPE, AND THAT IS THE WHOLE POINT.
|
||||
|
||||
DeviceId names the DEVICE hanging off this PC. A machine number is a
|
||||
different fact - "which bay is this" rather than "which device is this" -
|
||||
and the two coincide for exactly one family:
|
||||
|
||||
CMM bays the machine number IS the instrument id (CMM4), which is
|
||||
why cmmid.txt has always doubled as identity. Copying it
|
||||
is correct.
|
||||
|
||||
part markers the machine number is the OPERATION (0613), and several
|
||||
markers serve one operation. Copying it would name the
|
||||
operation as the device. The collector refuses that
|
||||
("asset X is not a Part Marker; not linked") because a
|
||||
machine filed under a device label reads as a marker
|
||||
everywhere downstream. So machine number is NEVER a
|
||||
source here.
|
||||
|
||||
Keyence, no per-bay id exists at all. asset-id.txt is the only
|
||||
Genspect, identity these have ever had.
|
||||
wax-trace
|
||||
|
||||
everything not a device bay. DeviceId is not consumed for these
|
||||
else pc-types, so writing one is a landmine for the day that
|
||||
pc-type gains a device spec. Writes nothing.
|
||||
|
||||
9999 is the unassigned placeholder and is skipped everywhere, matching
|
||||
Report-AssetToShopDB.ps1 and the server.
|
||||
|
||||
.PARAMETER Force
|
||||
Overwrite a DeviceId that is already set to a DIFFERENT value.
|
||||
|
||||
Off by default on purpose. asset-id.txt is written ONCE at imaging and
|
||||
nothing updates it when a device is swapped, so a stale file overwriting a
|
||||
corrected registry value would re-introduce the exact defect this move is
|
||||
meant to fix. A disagreement is reported and left alone for a human.
|
||||
|
||||
.PARAMETER PCType
|
||||
Override the pc-type instead of reading C:\Enrollment\pc-type.txt. For
|
||||
testing and for a bay whose file is missing.
|
||||
|
||||
.PARAMETER DeviceId
|
||||
Write this value directly, skipping resolution. Still honours -Force and
|
||||
the 9999 rule.
|
||||
|
||||
.PARAMETER LogPath
|
||||
Append a transcript here as well as writing to the pipeline.
|
||||
|
||||
.EXAMPLE
|
||||
.\Set-ShopDBDeviceId.ps1 -WhatIf
|
||||
Show what would be written on this bay without touching the registry.
|
||||
|
||||
.EXAMPLE
|
||||
.\Set-ShopDBDeviceId.ps1
|
||||
Seed DeviceId. Safe to run repeatedly and from a GE-Enforce PS1 entry.
|
||||
|
||||
.NOTES
|
||||
Exit codes:
|
||||
0 wrote a value, it was already correct, or this is not a device bay
|
||||
1 an error stopped the write
|
||||
2 an existing DeviceId disagrees with the resolved value; needs a human
|
||||
or -Force
|
||||
#>
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[switch]$Force,
|
||||
[string]$PCType,
|
||||
[string]$DeviceId,
|
||||
[string]$LogPath
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$RegPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
||||
$RegValueName = 'DeviceId'
|
||||
$Placeholder = '9999'
|
||||
|
||||
function Log {
|
||||
param([string]$Message)
|
||||
$line = "{0} [Set-ShopDBDeviceId] {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||
Write-Output $line
|
||||
if ($LogPath) {
|
||||
try { Add-Content -LiteralPath $LogPath -Value $line -ErrorAction Stop } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
function Read-FirstLine {
|
||||
# One line, trimmed, empty when absent or unreadable. A bay missing a file
|
||||
# is the normal case, not an error.
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return '' }
|
||||
try {
|
||||
$v = [string](Get-Content -LiteralPath $Path -First 1 -ErrorAction Stop)
|
||||
return $v.Trim()
|
||||
} catch {
|
||||
Log "WARN could not read ${Path}: $($_.Exception.Message)"
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MachineNoFromRegistry {
|
||||
# eDNC/NTLARS, WOW6432Node first, matching Report-AssetToShopDB.ps1.
|
||||
foreach ($rp in @(
|
||||
'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General',
|
||||
'HKLM:\SOFTWARE\GE Aircraft Engines\DNC\General'
|
||||
)) {
|
||||
try {
|
||||
if (Test-Path $rp) {
|
||||
$v = [string](Get-ItemProperty -Path $rp -Name MachineNo -ErrorAction Stop).MachineNo
|
||||
if ($v) {
|
||||
$v = $v.Trim()
|
||||
if ($v -and $v -ne $Placeholder) { return $v }
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Log "WARN could not read MachineNo from ${rp}: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
# Which pc-types drive a device, and whether the machine number is a legitimate
|
||||
# identity for that device. Mirrors SUBORDINATE_DEVICE_MAP in
|
||||
# plugins/computers/pctypemap.py. A site that added its own device pc-type
|
||||
# through a subordinatedevice_<pctype> setting adds it here too.
|
||||
$DeviceFamilies = @{
|
||||
'gea-shopfloor-cmm' = @{ Label = 'CMM'; MachineNoIsDevice = $true }
|
||||
'gea-shopfloor-keyence' = @{ Label = 'Vision System'; MachineNoIsDevice = $false }
|
||||
'gea-shopfloor-genspect' = @{ Label = 'Genspect'; MachineNoIsDevice = $false }
|
||||
'gea-shopfloor-waxtrace' = @{ Label = 'Form Tracer'; MachineNoIsDevice = $false }
|
||||
'gea-shopfloor-partmarker'= @{ Label = 'Part Marker'; MachineNoIsDevice = $false }
|
||||
}
|
||||
|
||||
# --- 1. what kind of bay is this -------------------------------------------
|
||||
|
||||
if (-not $PCType) { $PCType = Read-FirstLine 'C:\Enrollment\pc-type.txt' }
|
||||
|
||||
if (-not $PCType) {
|
||||
Log 'no pc-type on this PC (C:\Enrollment\pc-type.txt missing or empty); nothing to do'
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $DeviceFamilies.ContainsKey($PCType)) {
|
||||
Log "pc-type '$PCType' does not drive a device; DeviceId not written"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$family = $DeviceFamilies[$PCType]
|
||||
Log "pc-type '$PCType' drives a $($family.Label)"
|
||||
|
||||
# --- 2. resolve the identity ------------------------------------------------
|
||||
|
||||
$resolved = ''
|
||||
$source = ''
|
||||
|
||||
if ($DeviceId) {
|
||||
$resolved = $DeviceId.Trim()
|
||||
$source = '-DeviceId parameter'
|
||||
} else {
|
||||
# Most stable identity first, and the machine number only where it really
|
||||
# names the device.
|
||||
if ($family.MachineNoIsDevice) {
|
||||
$v = Read-FirstLine 'C:\Enrollment\cmm\cmmid.txt'
|
||||
if ($v -and $v -ne $Placeholder) { $resolved = $v; $source = 'C:\Enrollment\cmm\cmmid.txt' }
|
||||
|
||||
if (-not $resolved) {
|
||||
$v = Get-MachineNoFromRegistry
|
||||
if ($v) { $resolved = $v; $source = 'eDNC MachineNo registry' }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $resolved) {
|
||||
$v = Read-FirstLine 'C:\Enrollment\asset-id.txt'
|
||||
if ($v -and $v -ne $Placeholder) { $resolved = $v; $source = 'C:\Enrollment\asset-id.txt' }
|
||||
}
|
||||
|
||||
if (-not $resolved) {
|
||||
# The 0.12.0 name, before the file was generalised.
|
||||
$v = Read-FirstLine 'C:\Enrollment\measuringtool-id.txt'
|
||||
if ($v -and $v -ne $Placeholder) { $resolved = $v; $source = 'C:\Enrollment\measuringtool-id.txt' }
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $resolved) {
|
||||
# Nothing to copy. Silence is the right answer: the collector still adopts
|
||||
# or mints as it does today, and inventing a value here would name a device
|
||||
# that nobody can account for.
|
||||
Log "no identity found for this $($family.Label) bay; DeviceId not written"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($resolved -eq $Placeholder) {
|
||||
Log "resolved value is the $Placeholder placeholder; DeviceId not written"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Log "resolved DeviceId '$resolved' from $source"
|
||||
|
||||
# --- 3. write it ------------------------------------------------------------
|
||||
|
||||
$existing = ''
|
||||
try {
|
||||
if (Test-Path $RegPath) {
|
||||
$prop = Get-ItemProperty -Path $RegPath -Name $RegValueName -ErrorAction SilentlyContinue
|
||||
if ($prop) { $existing = [string]$prop.$RegValueName; $existing = $existing.Trim() }
|
||||
}
|
||||
} catch {
|
||||
Log "WARN could not read existing ${RegValueName}: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($existing -eq $resolved) {
|
||||
Log "DeviceId already '$resolved'; nothing to do"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($existing -and -not $Force) {
|
||||
# The registry value wins by default. C:\Enrollment is written once at
|
||||
# imaging and never updated on a device swap, so the file is the more
|
||||
# likely one to be wrong.
|
||||
Log "REFUSING to overwrite: DeviceId is '$existing', $source says '$resolved'"
|
||||
Log 'Re-run with -Force if the file is right, or fix the file if the registry is.'
|
||||
exit 2
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path $RegPath)) {
|
||||
if ($PSCmdlet.ShouldProcess($RegPath, 'Create registry key')) {
|
||||
New-Item -Path $RegPath -Force | Out-Null
|
||||
Log "created $RegPath"
|
||||
}
|
||||
}
|
||||
$action = if ($existing) { "Set $RegValueName '$existing' -> '$resolved'" }
|
||||
else { "Set $RegValueName '$resolved'" }
|
||||
if ($PSCmdlet.ShouldProcess($RegPath, $action)) {
|
||||
New-ItemProperty -Path $RegPath -Name $RegValueName -Value $resolved `
|
||||
-PropertyType String -Force | Out-Null
|
||||
Log "wrote $RegPath\$RegValueName = '$resolved'"
|
||||
} else {
|
||||
Log "WHATIF would write $RegPath\$RegValueName = '$resolved'"
|
||||
}
|
||||
} catch {
|
||||
Log "ERROR could not write ${RegValueName}: $($_.Exception.Message)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -174,7 +174,9 @@ async function loadData() {
|
||||
}
|
||||
|
||||
if (types.status === 'fulfilled') {
|
||||
pctypes.value = types.value.data.data || []
|
||||
// Plain array, same as applications below. Reading .data.data threw
|
||||
// inside this try, so apps, actions and monitors never got assigned.
|
||||
pctypes.value = types.value || []
|
||||
} else {
|
||||
failures.push('PC types')
|
||||
console.error('Error loading PC types:', types.reason)
|
||||
|
||||
@@ -1121,7 +1121,10 @@ class EndpointsPlugin(BasePlugin):
|
||||
if 'osinfo' in payload:
|
||||
state.osinfojson = _as_json(payload.get('osinfo'))
|
||||
if 'domain' in payload:
|
||||
state.domain = payload.get('domain')
|
||||
# Clamped like every other String column here: an over-long value
|
||||
# aborts the whole collector transaction under STRICT_TRANS_TABLES,
|
||||
# so one bad agent report would drop a machine's entire telemetry.
|
||||
state.domain = _clamp(payload.get('domain'), 255)
|
||||
if 'pendingreboot' in payload:
|
||||
state.pendingreboot = payload.get('pendingreboot')
|
||||
if 'diskhealth' in payload:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Knowledge Base API endpoints."""
|
||||
|
||||
from flask import Blueprint, request
|
||||
from flask_jwt_extended import jwt_required
|
||||
|
||||
from shopdb.api import (
|
||||
intarg,
|
||||
@@ -180,21 +179,29 @@ def get_article(link_id: int):
|
||||
|
||||
|
||||
@knowledgebase_bp.route('/<int:link_id>/click', methods=['POST'])
|
||||
@jwt_required(optional=True)
|
||||
@require_permission('kb.view')
|
||||
def track_click(link_id: int):
|
||||
"""Increment click counter and return the URL to redirect to."""
|
||||
article = db.session.get(KnowledgeBase, link_id)
|
||||
"""Increment the click counter for an article the caller is allowed to read.
|
||||
|
||||
if not article or not article.isactive:
|
||||
Gated on kb.view like every other read of this table: an anonymous caller
|
||||
walking link ids must not be able to probe which articles exist, drive the
|
||||
counters that order the list, or spend a DB transaction per request.
|
||||
|
||||
Restricted to the same visible set as the list/detail reads so an article
|
||||
about a retired application is not reachable here either.
|
||||
|
||||
The linkurl is deliberately not echoed back: whoever is permitted to click
|
||||
already received it in the list or detail response the click came from.
|
||||
"""
|
||||
article = _visible_articles().filter(KnowledgeBase.linkid == link_id).first()
|
||||
|
||||
if not article:
|
||||
return error_response(ErrorCodes.NOT_FOUND, 'Article not found', http_code=404)
|
||||
|
||||
article.increment_clicks()
|
||||
db.session.commit()
|
||||
|
||||
return success_response({
|
||||
'linkurl': article.linkurl,
|
||||
'clicks': article.clicks
|
||||
})
|
||||
return success_response({'clicks': article.clicks})
|
||||
|
||||
|
||||
@knowledgebase_bp.route('', methods=['POST'])
|
||||
|
||||
Reference in New Issue
Block a user