An ACCEPTED ADR ended with a stray tool-call fragment - it said nothing to a reader and everything about how the document was produced, which is not what a decision record is for. And the cutover playbook carried a real internal IP in a file otherwise correctly scrubbed to documentation ranges. Both were published. The publishability gate now catches all three shapes, so the next one fails a build instead of reaching the wiki. The adoption guide also told a site to copy two files that ship from nowhere: the asset reporter and EventSaver are both on the reference site's imaging share, not in this repository, and the guide never said so - it could not be followed as written. It now states, per artifact, where each comes from, which of them are pending a move into this repository, and that EventSaver can be rebuilt from its one source file rather than taken on trust as a binary.
446 lines
18 KiB
Markdown
446 lines
18 KiB
Markdown
# Adopting the fleet tools at another site
|
|
|
|
Two tools run on shop-floor PCs and talk to a ShopDB instance: the **asset
|
|
reporter**, which posts what a PC is to the collector API, and **EventSaver**,
|
|
a screensaver that shows a slideshow served by ShopDB. Both are designed to be
|
|
repointed at another site's server. This page is how.
|
|
|
|
Neither tool is site-specific by design. What is site-specific is the URL, the
|
|
API key, and which PCs it runs on - and all three are inputs, not code.
|
|
|
|
> **Prerequisite.** The reporter's `-ApiUrl` parameter still carries a default
|
|
> pointing at the reference site's server. Adopt it with an explicit `-ApiUrl`
|
|
> (every example below does) until that default is removed. See
|
|
> [ADR-015](adr/ADR-015-site-specific-configuration.md).
|
|
|
|
## Where these files come from
|
|
|
|
The commands below copy files. This is where each one is obtained, because they
|
|
do not all ship from the same place and two of them are not in this repository
|
|
at all yet.
|
|
|
|
| Artifact | Where it is today |
|
|
|---|---|
|
|
| GE-Enforce client (`Install-GEEnforce.ps1`, `Invoke-ShopdbEnforce.ps1`, `ShopdbEnforceClient.psm1`) | This repository, `plugins/geenforce/client/`. Present on any installed server under the install directory. |
|
|
| `Report-AssetToShopDB.ps1` | **Not in this repository.** It lives on the reference site's imaging share and is provided on request. It is planned to move to `plugins/computers/client/` so it versions with the collector contract it implements. |
|
|
| EventSaver (`EventSaver.scr`, `EventSaver.ini`, `EventSaver.cs`) | **Not in this repository.** Provided on request; the source is a single C# file that builds with the in-box .NET Framework compiler, so a site can rebuild it rather than trust a binary. See [EVENTSAVER.md](EVENTSAVER.md). |
|
|
|
|
Ask the maintainers for the two that are not here. A site that would rather not
|
|
run a binary it cannot rebuild should take EventSaver's source and compile it
|
|
locally - the build needs no SDK and is one command.
|
|
|
|
---
|
|
|
|
## Part 1: the asset reporter
|
|
|
|
### What it does
|
|
|
|
Runs as SYSTEM, collects hostname, BIOS serial, PC type, logged-in user, IP
|
|
addresses and the machine number where one is configured, and POSTs them to:
|
|
|
|
```
|
|
POST https://<your-shopdb>/api/collector/computers
|
|
X-API-Key: <collector token>
|
|
Content-Type: application/json
|
|
```
|
|
|
|
The payload contract is [COLLECTOR-INTEGRATION.md](COLLECTOR-INTEGRATION.md) and
|
|
[ADR-006](adr/ADR-006-collector-contract.md). A field your site does not collect
|
|
is simply absent; the server upserts on `hostname` and leaves the rest alone.
|
|
|
|
### What you must provide
|
|
|
|
| Input | Where it comes from |
|
|
|---|---|
|
|
| Server URL | `-ApiUrl https://<your-shopdb>/api/collector/computers` |
|
|
| API key | `-ApiKey`, or `HKLM:\SOFTWARE\GE\ShopDB` value `CollectorKey` |
|
|
|
|
### Minting the key on your ShopDB
|
|
|
|
Create a **managed personal access token scoped to `collector.ingest` and
|
|
nothing else**, and use it as the `X-API-Key` value. That scope authorises the
|
|
collector ingest API and no normal route, so a token recovered off a shop-floor
|
|
PC cannot read your asset register. A shared `COLLECTOR_API_KEY` environment
|
|
variable also works and is simpler for a pilot, but it cannot be rotated per
|
|
fleet or revoked individually.
|
|
|
|
### Deploying it
|
|
|
|
The three paths below are alternatives - pick one. All of them end with the same
|
|
two facts on the PC: the script exists somewhere it can run from, and the key is
|
|
in the registry.
|
|
|
|
#### Option A: Microsoft Intune
|
|
|
|
**A1. The key, as a platform script.** Devices > Scripts and remediations >
|
|
Platform scripts > Add > Windows 10 and later. Run as SYSTEM, do not run in the
|
|
64-bit context only.
|
|
|
|
```powershell
|
|
# Set-ShopdbCollectorKey.ps1 (Intune platform script, runs once per device)
|
|
$RegPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
|
$Key = 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN'
|
|
if (-not (Test-Path $RegPath)) { New-Item -Path $RegPath -Force | Out-Null }
|
|
$current = (Get-ItemProperty -Path $RegPath -Name CollectorKey -ErrorAction SilentlyContinue).CollectorKey
|
|
if ($current -ne $Key) { Set-ItemProperty -Path $RegPath -Name CollectorKey -Value $Key }
|
|
exit 0
|
|
```
|
|
|
|
Putting a token in an Intune script body means anyone who can read the Intune
|
|
configuration can read the token. That is an argument for the `collector.ingest`
|
|
scope, not against Intune: the blast radius of that token is one API.
|
|
|
|
**A2. The reporter, as a remediation on a schedule.** Devices > Scripts and
|
|
remediations > Remediations. A remediation is the natural fit because it re-runs
|
|
on a schedule, which is exactly what reporting is.
|
|
|
|
*Detection script* - forces the remediation to run every cycle:
|
|
|
|
```powershell
|
|
# Always "not compliant": reporting is not a state to converge on, it is an
|
|
# event that should happen on every schedule tick.
|
|
exit 1
|
|
```
|
|
|
|
*Remediation script* - the reporter itself, with your URL:
|
|
|
|
```powershell
|
|
& "$PSScriptRoot\Report-AssetToShopDB.ps1" `
|
|
-ApiUrl 'https://shopdb.example.net/api/collector/computers'
|
|
exit 0
|
|
```
|
|
|
|
Assign it with a daily schedule. Run as SYSTEM, 64-bit.
|
|
|
|
**A3. Intune with DSC (Machine Configuration).** If your estate is already
|
|
governed by Azure Machine Configuration, express the *key* as configuration and
|
|
leave the *reporting* to a scheduled task, because a report is an event and DSC
|
|
converges state:
|
|
|
|
```powershell
|
|
Configuration ShopdbCollector
|
|
{
|
|
Import-DscResource -ModuleName PSDesiredStateConfiguration
|
|
|
|
Node localhost
|
|
{
|
|
Registry CollectorKey
|
|
{
|
|
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\GE\ShopDB'
|
|
ValueName = 'CollectorKey'
|
|
ValueData = 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN'
|
|
ValueType = 'String'
|
|
Ensure = 'Present'
|
|
}
|
|
|
|
Registry CollectorUrl
|
|
{
|
|
Key = 'HKEY_LOCAL_MACHINE\SOFTWARE\GE\ShopDB'
|
|
ValueName = 'BaseUrl'
|
|
ValueData = 'https://shopdb.example.net'
|
|
ValueType = 'String'
|
|
Ensure = 'Present'
|
|
}
|
|
|
|
Script ReportingTask
|
|
{
|
|
GetScript = { @{ Result = (Get-ScheduledTask -TaskName 'ShopDB asset report' -ErrorAction SilentlyContinue) } }
|
|
TestScript = { [bool](Get-ScheduledTask -TaskName 'ShopDB asset report' -ErrorAction SilentlyContinue) }
|
|
SetScript = {
|
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
|
-Argument '-NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\ShopDB\Report-AssetToShopDB.ps1" -ApiUrl "https://shopdb.example.net/api/collector/computers"'
|
|
$trigger = New-ScheduledTaskTrigger -Daily -At 7am
|
|
Register-ScheduledTask -TaskName 'ShopDB asset report' -Action $action `
|
|
-Trigger $trigger -User 'SYSTEM' -RunLevel Highest -Force
|
|
}
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
#### Option B: GE-Enforce
|
|
|
|
If you already run the GE-Enforce client, this is one manifest entry, and it is
|
|
how the reference site does it. `Type: PS1` with `DetectionMethod: Always` runs
|
|
the script from the share every cycle with no local copy and no hash to bump
|
|
when the script changes.
|
|
|
|
```json
|
|
{
|
|
"_comment": "Reports host, BIOS serial, pc-type, logged-in user, IPs and machine number to ShopDB every cycle, as SYSTEM, straight off the share.",
|
|
"Name": "Report asset to ShopDB",
|
|
"Type": "PS1",
|
|
"Script": "apps/Report-AssetToShopDB.ps1",
|
|
"Args": "-ApiUrl https://shopdb.example.net/api/collector/computers",
|
|
"DetectionMethod": "Always"
|
|
}
|
|
```
|
|
|
|
Provision the key with a second entry, ordered **before** it, so the key is
|
|
present the same cycle the first report fires:
|
|
|
|
```json
|
|
{
|
|
"_comment": "Writes the collector key from an ACL'd share file. The token is NOT in this manifest - manifests sync broadly. Rotate by replacing configs/collector-key.txt on the share; every PC picks it up next cycle.",
|
|
"Name": "Provision the ShopDB collector key",
|
|
"Type": "PS1",
|
|
"Script": "scripts/Set-CollectorKey.ps1",
|
|
"DetectionMethod": "Always"
|
|
}
|
|
```
|
|
|
|
Keep the token in `configs/collector-key.txt` on your share, locked to SYSTEM
|
|
and administrators - not in the manifest JSON.
|
|
|
|
#### Option C: manual
|
|
|
|
For a pilot, a single bay, or an estate with no management plane:
|
|
|
|
```powershell
|
|
# As administrator, once per PC.
|
|
New-Item -Path 'C:\ProgramData\ShopDB' -ItemType Directory -Force | Out-Null
|
|
Copy-Item .\Report-AssetToShopDB.ps1 'C:\ProgramData\ShopDB\'
|
|
|
|
New-Item -Path 'HKLM:\SOFTWARE\GE\ShopDB' -Force | Out-Null
|
|
Set-ItemProperty -Path 'HKLM:\SOFTWARE\GE\ShopDB' -Name CollectorKey `
|
|
-Value 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN'
|
|
|
|
$action = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument @'
|
|
-NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\ShopDB\Report-AssetToShopDB.ps1" -ApiUrl "https://shopdb.example.net/api/collector/computers"
|
|
'@
|
|
$trigger = New-ScheduledTaskTrigger -Daily -At 7am
|
|
Register-ScheduledTask -TaskName 'ShopDB asset report' -Action $action -Trigger $trigger `
|
|
-User 'SYSTEM' -RunLevel Highest -Force
|
|
|
|
Start-ScheduledTask -TaskName 'ShopDB asset report'
|
|
```
|
|
|
|
Lock down `C:\ProgramData\ShopDB` if you put it there: a script a user can edit,
|
|
run by SYSTEM on a schedule, is a local privilege escalation. Break inheritance
|
|
and grant Administrators and SYSTEM only.
|
|
|
|
### Verifying
|
|
|
|
```powershell
|
|
Invoke-RestMethod -Uri 'https://shopdb.example.net/api/computers/by-hostname/<HOSTNAME>'
|
|
```
|
|
|
|
The PC should exist with its serial, type and last-reported timestamp. The
|
|
script also logs to `C:\Logs\Shopfloor`.
|
|
|
|
---
|
|
|
|
## Part 2: EventSaver
|
|
|
|
### What it does
|
|
|
|
A Windows screensaver (`EventSaver.scr`) that shows a slideshow. It reads
|
|
`EventSaver.ini` from the directory it lives in, **on every launch**, so
|
|
retargeting it needs no recompile:
|
|
|
|
```ini
|
|
# HTTP mode (recommended): pull slides from ShopDB, cache locally. No share.
|
|
url=https://shopdb.example.net/api/slides/feed?surface=shopfloor
|
|
|
|
# Folder mode (fallback): used only when url is blank. SMB or local path.
|
|
# folder=\\fileserver\shopfloor\tv
|
|
|
|
interval=10 # seconds per image, when a slide carries no time of its own
|
|
shuffle=0 # 1 = random, 0 = ordered
|
|
fadems=600 # crossfade length, reserved
|
|
```
|
|
|
|
HTTP mode is the one to adopt: slides are managed in ShopDB (Slides), and the
|
|
screensaver caches them locally, so a PC that cannot reach the server keeps
|
|
showing the last set instead of going black.
|
|
|
|
### The two things that catch people out
|
|
|
|
**The compiled-in fallback is not yours.** If `EventSaver.ini` is missing or
|
|
both `url` and `folder` are blank, the binary falls back to a folder path
|
|
compiled into `EventSaver.cs`, which is the reference site's file server. Ship
|
|
the ini. A missing ini is not a neutral default.
|
|
|
|
**If you enforce the ini by hash, your own edits revert.** The reference
|
|
manifest pins `EventSaver.ini` with `DetectionMethod: Hash`, which is what makes
|
|
a retune propagate to the fleet automatically - and equally means a hand edit on
|
|
one PC is undone next cycle. That is the feature working. Edit the copy on
|
|
**your** share and update `DetectionValue` to its new SHA-256:
|
|
|
|
```powershell
|
|
(Get-FileHash .\configs\EventSaver.ini -Algorithm SHA256).Hash
|
|
```
|
|
|
|
### Deploying it
|
|
|
|
#### Option A: Intune
|
|
|
|
Package the three files as a Win32 app (`EventSaver.scr`, `EventSaver.ini`, and
|
|
the enable script) with this install command:
|
|
|
|
```powershell
|
|
# install.ps1 for the Win32 app
|
|
Copy-Item .\EventSaver.scr 'C:\Windows\System32\EventSaver.scr' -Force
|
|
Copy-Item .\EventSaver.ini 'C:\Windows\System32\EventSaver.ini' -Force
|
|
exit 0
|
|
```
|
|
|
|
Detection rule: file `C:\Windows\System32\EventSaver.scr` exists **and** matches
|
|
the expected SHA-256, so a changed binary reinstalls.
|
|
|
|
The screensaver itself is per-user, so set it with a configuration profile
|
|
(Settings catalog > Screen saver) or a user-context script:
|
|
|
|
```powershell
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name SCRNSAVE.EXE -Value 'C:\Windows\System32\EventSaver.scr'
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '1'
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '480'
|
|
```
|
|
|
|
With Machine Configuration/DSC, express the two files with `File` resources and
|
|
the screensaver with a `Registry` resource in the user hive - noting that DSC
|
|
runs as SYSTEM, so a per-user setting needs the logged-on user's hive or a
|
|
default-user template, which is usually more trouble than a login script.
|
|
|
|
#### Option B: GE-Enforce
|
|
|
|
Three entries, which is exactly how the reference fleet does it. Note the
|
|
per-PC-type targeting: a kiosk showing a live dashboard must **not** get a
|
|
screensaver over the top of it.
|
|
|
|
```json
|
|
{
|
|
"Name": "EventSaver screensaver (binary)",
|
|
"Type": "File",
|
|
"Source": "apps/EventSaver.scr",
|
|
"Destination": "C:\\Windows\\System32\\EventSaver.scr",
|
|
"DetectionMethod": "Hash",
|
|
"DetectionPath": "C:\\Windows\\System32\\EventSaver.scr",
|
|
"DetectionValue": "<sha256 of your staged .scr>",
|
|
"PCTypes": ["gea-shopfloor-common"]
|
|
},
|
|
{
|
|
"Name": "EventSaver screensaver (config)",
|
|
"Type": "File",
|
|
"Source": "configs/EventSaver.ini",
|
|
"Destination": "C:\\Windows\\System32\\EventSaver.ini",
|
|
"DetectionMethod": "Hash",
|
|
"DetectionPath": "C:\\Windows\\System32\\EventSaver.ini",
|
|
"DetectionValue": "<sha256 of YOUR .ini, pointing at YOUR shopdb>",
|
|
"PCTypes": ["gea-shopfloor-common"]
|
|
},
|
|
{
|
|
"Name": "EventSaver enable (per-user screensaver)",
|
|
"Type": "PS1",
|
|
"Script": "scripts/Set-EventSaverScreensaver.ps1",
|
|
"Args": "-TimeoutSeconds 480",
|
|
"DetectionMethod": "Always",
|
|
"PCTypes": ["gea-shopfloor-common"]
|
|
}
|
|
```
|
|
|
|
#### Option C: manual
|
|
|
|
```powershell
|
|
Copy-Item .\EventSaver.scr 'C:\Windows\System32\' -Force
|
|
Copy-Item .\EventSaver.ini 'C:\Windows\System32\' -Force # with YOUR url=
|
|
|
|
# Per user, in the user's own session:
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name SCRNSAVE.EXE -Value 'C:\Windows\System32\EventSaver.scr'
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveActive -Value '1'
|
|
Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name ScreenSaveTimeOut -Value '480'
|
|
```
|
|
|
|
Run `EventSaver.scr /c` to see which source it resolved and where it is caching.
|
|
|
|
### Verifying
|
|
|
|
Open the feed in a browser from the PC itself:
|
|
|
|
```
|
|
https://shopdb.example.net/api/slides/feed?surface=shopfloor
|
|
```
|
|
|
|
It is a public endpoint - the screensaver has no credentials - and returns the
|
|
playlist with per-slide durations. An empty `slides` array means slides have not
|
|
been uploaded for that surface, not that the PC is misconfigured.
|
|
|
|
---
|
|
|
|
---
|
|
|
|
## Part 3: bootstrapping GE-Enforce itself
|
|
|
|
The two parts above assume the tools are already on the PC. GE-Enforce is what
|
|
puts them there and keeps them there - so the question is how GE-Enforce gets
|
|
onto a bay in the first place. Two paths are supported, and they are the two
|
|
that actually occur: imaging time, and a management plane such as Intune.
|
|
|
|
Whichever you use, the end state is the same three things:
|
|
|
|
1. The client files in `C:\Program Files\GE\Shopfloor`.
|
|
2. `HKLM:\SOFTWARE\GE\ShopDB` carrying `BaseUrl`, and `ApiToken` where the scope
|
|
is served over HTTPS.
|
|
3. A scheduled task running the enforce cycle as SYSTEM.
|
|
|
|
### At imaging time (the usual path)
|
|
|
|
The shop-floor imaging pipeline registers the enforce task as its last step,
|
|
once the PC type is known - the type is what decides which manifest scope the
|
|
bay enforces. Self-contained types are skipped deliberately: a display kiosk
|
|
gets its configuration at imaging time and no share, so registering an enforce
|
|
task on one would give it a cycle with nothing to do.
|
|
|
|
If you are building your own imaging pipeline, the equivalent step is: copy the
|
|
engine into a runtime directory, write the PC type where the client can read it,
|
|
and register the task. That is what `Install-GEEnforce.ps1` does in one call.
|
|
|
|
### Through Intune
|
|
|
|
`Install-GEEnforce.ps1` is parameterised for exactly this, and this is how the
|
|
display cohort is deployed today - those PCs are Entra-joined, have no file
|
|
share, and fetch their manifest over HTTPS:
|
|
|
|
```powershell
|
|
.\Install-GEEnforce.ps1 `
|
|
-PCType 'gea-shopfloor-common' `
|
|
-ShopdbUrl 'https://shopdb.example.net' `
|
|
-ShopdbToken 'shopdb_pat_REPLACE_WITH_YOUR_TOKEN'
|
|
```
|
|
|
|
Package it as a Win32 app with the client files. Detection rule: the scheduled
|
|
task exists **and** `HKLM:\SOFTWARE\GE\ShopDB\BaseUrl` matches your server, so a
|
|
PC that was imaged for another site is repaired rather than skipped.
|
|
|
|
The token is a managed service token scoped to the GE-Enforce fetch scope, not
|
|
an admin credential. Scope it that way and a token read off a bay buys the
|
|
reader a manifest they could have read anyway.
|
|
|
|
### With DSC (Machine Configuration)
|
|
|
|
There is no shipped DSC configuration, but nothing about the end state resists
|
|
one - it is two registry values, a set of files and a scheduled task, which is
|
|
ordinary DSC territory. Express it the same way as the reporter example above:
|
|
`Registry` resources for `BaseUrl` and `ApiToken`, `File` resources for the
|
|
client, and a `Script` resource that registers the task.
|
|
|
|
One caution. DSC converges state on a schedule and GE-Enforce is itself a
|
|
convergence loop, so running both against the same PC means two things fighting
|
|
to own the same configuration. Use DSC to install and configure the client, and
|
|
let GE-Enforce own everything downstream of that. Deciding which tool owns what
|
|
is the whole job; splitting it by layer is what keeps it answerable.
|
|
|
|
## Which path to choose
|
|
|
|
| You have | Use |
|
|
|---|---|
|
|
| Intune, no GE-Enforce | Option A. A remediation for the reporter, a Win32 app for EventSaver. |
|
|
| GE-Enforce | Option B. One manifest entry each, and the fleet converges on its own cycle. |
|
|
| Neither, or a pilot of a few bays | Option C, then move to A or B once it earns its place. |
|
|
|
|
GE-Enforce and Intune are not exclusive. The reference site uses Intune for
|
|
enrolment and imaging and GE-Enforce for shop-floor configuration state, because
|
|
Intune is not on the shop floor network path in the same way. If you have both,
|
|
the deciding question is which one you would look at first to answer "why is
|
|
this bay wrong".
|