docs(deploy): vd03 upgrade-readiness pack b8b4c18f -> master (#793)
v2-ci / unit-tests (push) Waiting to run
v2-ci / unit-tests (push) Waiting to run
Read-only ConfigDb preflight (scripts/preflight/vd03-preupgrade-b8b4c18f-configdb-checks.sql) covering every deploy gate added since the deployed build: VirtualTag/Calculation effective triggers (#783), VT->VT reads and VirtualTagDependencyCycle (#791), newly forbidden script APIs (#792, E2), credential-reference namespace / other-instance / reserved-name rules (#815 #835 #836 #848 #849 #850), MTConnectMultipleDevices, Sql query/write gates (#781), Heidenhain, UNS effective-name collisions and the FOCAS renames/value changes. Verified on a throwaway b8b4c18f-schema database (every check fires on seeded rows) and clean against the docker-dev config. Upgrade notes (docs/deploy/vd03-upgrade-b8b4c18f-to-master.md): the EnableSnapshotIsolation migration and how to apply it by hand, the env-var audit (Security:ApiKeys:Pepper is now required and absent on the box; JWT key validated at start), SDK 2.0.1 / CA KeyUsage, the ScadaBridge-visible changes, swap/rollback and post-deploy checks. Claude-Session: https://claude.ai/code/session_01GT67AD99zQQqpLCVm3p7xk
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
# wonder-app-vd03 upgrade: `b8b4c18f` → master `363f0d81`
|
||||
|
||||
Upgrade-readiness notes for the DEV box wonder-app-vd03. The box runs the `OtOpcUaHost` service from
|
||||
`E:\ApiInstall\OtOpcUa`: one fused admin+driver node with one FOCAS driver (Makino Z-34184). The ConfigDb
|
||||
is `OtOpcUaConfig` on the box's SQL instance (`docs/deployments/wonder-app-vd03-makino-z-34184.md`). Tracking
|
||||
issue: Gitea #793.
|
||||
|
||||
**Baseline.** The deployed build is `b8b4c18f` (2026-09-03), not the `e82881c1` recorded in the 2026-08-11
|
||||
notes. The evidence:
|
||||
- All 409 source documents in six deployed PDBs match `b8b4c18f`.
|
||||
- `deps.json` pins `LocalDb 0.3.4` and `MxGateway.Client 0.2.1`, which only `b8b4c18f` used.
|
||||
|
||||
Everything below is the `b8b4c18f..master` delta. The ConfigDb schema is the same at both commits. The last
|
||||
migration at both is `20260731162342_AddApiKeyTable`.
|
||||
|
||||
> ⛔ The remote-exec hold on the wonder boxes still applies (root `CLAUDE.md`): no psbridge and no WinRM. The
|
||||
> owner runs every step on the box. SMB copies and direct SQL logins are allowed.
|
||||
|
||||
## Owner actions, in order
|
||||
|
||||
| # | Action | Blocks | Source |
|
||||
|---|---|---|---|
|
||||
| 1 | Run `scripts/preflight/vd03-preupgrade-b8b4c18f-configdb-checks.sql` against `OtOpcUaConfig`, then fix every `attention` + `blocking = YES` row. | The first deploy on master | §g |
|
||||
| 2 | **Add `Security__ApiKeys__Pepper`** to the service environment (at least 16 characters, or a `${secret:}` reference). The box does not have this setting. | **Host start**: the host exits with code 1 | §b |
|
||||
| 3 | Check that `Security__Jwt__SigningKey` is at least 32 UTF-8 bytes. | Host start | §b |
|
||||
| 4 | Apply the `EnableSnapshotIsolation` migration to `OtOpcUaConfig`. | Every deploy (SQL error 3952) | §a |
|
||||
| 5 | Check the 22 FOCAS tags for the `MaxRpm` → `MotorSpeedPct` and `AA_1/AA_2` → `AA6/AA7` renames, and for `String` on MACRO/PMC/PARAM addresses. Tell ScadaBridge about the FOCAS value changes. | Client-visible | §d, check 13 |
|
||||
| 6 | Make the #782 go/hold decision for SDK 2.0.1. The rollout runbook still marks it **HOLD**. | Policy | §c |
|
||||
|
||||
## (a) ConfigDb migration
|
||||
|
||||
- **One migration is in range:** `20261022102459_EnableSnapshotIsolation`, from commit `73e74e27`.
|
||||
- It runs `ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON` with `suppressTransaction: true`.
|
||||
- It changes no tables. `Down` is deliberately a no-op.
|
||||
- Source: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/20261022102459_EnableSnapshotIsolation.cs`.
|
||||
- **Why it is required.** `DraftSnapshotFactory.FromConfigDbAsync` now reads the whole draft inside one
|
||||
`IsolationLevel.Snapshot` transaction. Without the option, the first read fails with SQL error 3952 and the
|
||||
deploy is rejected. Source: the class remarks in `Configuration/Validation/DraftSnapshotFactory.cs`.
|
||||
- **The migration is not applied automatically.** The host never migrates the ConfigDb: `Program.cs:129` only
|
||||
migrates the secrets store. docker-dev runs a one-shot `migrator` container because "production owns schema
|
||||
changes" (`docker-dev/README.md`, `docker-dev/docker-compose.yml`). The owner applies it by hand, using either
|
||||
of these:
|
||||
- **Option 1:** `dotnet ef database update 20261022102459_EnableSnapshotIsolation` from
|
||||
`src/Core/ZB.MOM.WW.OtOpcUa.Configuration`, with `OTOPCUA_CONFIG_CONNECTION` pointing at `OtOpcUaConfig`
|
||||
(`DesignTimeDbContextFactory`, `docs/Configuration.md`).
|
||||
- **Option 2:** run this idempotent script in SSMS. It is the output of
|
||||
`dotnet ef migrations script 20260731162342_AddApiKeyTable 20261022102459_EnableSnapshotIsolation --idempotent`:
|
||||
```sql
|
||||
IF NOT EXISTS (SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'20261022102459_EnableSnapshotIsolation')
|
||||
BEGIN ALTER DATABASE CURRENT SET ALLOW_SNAPSHOT_ISOLATION ON; END;
|
||||
GO
|
||||
IF NOT EXISTS (SELECT * FROM [__EFMigrationsHistory] WHERE [MigrationId] = N'20261022102459_EnableSnapshotIsolation')
|
||||
BEGIN INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion]) VALUES (N'20261022102459_EnableSnapshotIsolation', N'10.0.7'); END;
|
||||
GO
|
||||
```
|
||||
- **Login and timing.** The login needs `ALTER` on the database. Turning the option on waits for in-flight
|
||||
transactions but does not need exclusive access, so it can run before the binary swap while the old build is
|
||||
still up. **The old build is unaffected by it:** it uses no `IsolationLevel` and `ALLOW_SNAPSHOT_ISOLATION`
|
||||
does not change default reads. A rollback needs no schema undo.
|
||||
|
||||
## (b) Configuration: environment variables and appsettings
|
||||
|
||||
**The 23 service environment variables against master** (names only; values were not inspected):
|
||||
|
||||
| Variable | On master |
|
||||
|---|---|
|
||||
| `ASPNETCORE_URLS`, `Cluster__Hostname`, `Cluster__Port`, `Cluster__PublicHostname`, `Cluster__SeedNodes__0`, `ConnectionStrings__ConfigDb`, `DOTNET_ENVIRONMENT`, `LocalDb__Path`, `Security__Auth__DisableLogin`, `Security__DeployApiKey` | Unchanged. `Security:DeployApiKey` now also gates the new `GET /api/deployments/{id}` (`AdminUI/Api/DeployApiEndpoints.cs`). |
|
||||
| `Cluster__Roles__0/1` + `OTOPCUA_ROLES` | Unchanged (`EffectiveRoles` is already in the baseline). `Cluster:Roles` wins. If the two sets disagree, master logs a Warning. If a role were a `cluster-…` role, #678.6 (`f292cf3b`) now still loads `appsettings.admin-driver.json` (`docs/Configuration.md`). |
|
||||
| `Secrets__SqlitePath` | Unchanged. The Secrets 0.6.2 absolute, outside-content-root path rule (`b037cc7a`) is **already in the baseline**, and the box boots with it. |
|
||||
| `Security__Jwt__SigningKey/Issuer/Audience` | **Re-interpreted: now validated at start** (`f72af76e`, `3ddb6876`; `Security/Jwt/JwtOptions.Validate`). When a key is set, it must be at least 32 UTF-8 bytes, and Issuer/Audience must be non-blank, or the host does not start. Bearer validation is now the first-party `AddJwtBearer`, pinned to HS256. |
|
||||
| `ServerHistorian__Enabled/Endpoint/ApiKey/UseTls` | Same keys. The HistorianGateway client goes 0.3.0 → 1.0.0 (`230cef4e`, historiangw#131). On `UseTls=false`, the cert options are ignored, not refused. A partially valid `CaCertificatePath` bundle now fails client creation. **Not determined:** the oldest gateway build that master works with. If `Enabled=true` on the box, check the box's gateway version first. |
|
||||
| `GALAXY_MXGW_API_KEY` | Not read by any code directly. A Galaxy config that references `env:GALAXY_MXGW_API_KEY` is **in scope** for #815: `GALAXY_MXGW_` is an allowed Galaxy prefix and `API_KEY` matches `[A-Z0-9_]`. It is also **allowed** under #835/#849 as a shared name, so the deploy gate does not refuse it. It would only collide with a Galaxy driver whose id or name has the env form `API` or `API_KEY` (`DriverCredentialPolicy.cs`, the `GalaxyScope` definition and the comment at line 374). The box has no Galaxy driver. The 2026-08-11 notes say this key was already rejected by the gateway. |
|
||||
| `Authentication__Ldap__DevStubMode` | **Dead at both commits.** No code reads an `Authentication:` section; the live key is `Security:Ldap:DevStubMode`, which the deployed `appsettings.admin-driver.json` sets to `false`, with `Transport=Ldaps`. Harmless. |
|
||||
|
||||
**New settings master needs that are not in that list:**
|
||||
|
||||
- **`Security__ApiKeys__Pepper`: REQUIRED. It is missing on the box.**
|
||||
- `ApiKeyOptionsValidator` (`f72af76e`, `Host/Configuration/ApiKeyOptionsValidator.cs`) fails host start on
|
||||
any admin or driver node when the pepper is blank or shorter than 16 characters.
|
||||
- The box has no pepper in its environment and none in the deployed `appsettings*.json`. Without it the
|
||||
upgraded service exits during start-up. Because that happens before Serilog starts, the reason only reaches
|
||||
stderr and the Event Log.
|
||||
- On `b8b4c18f` an empty pepper already made minting refuse and verification reject every key
|
||||
(`ApiKeySecret.cs`, `ApiKeyOpcUaUserAuthenticator.cs`). So no working data-plane API keys exist today, and
|
||||
choosing a fresh pepper invalidates nothing. Check 14 counts the `ApiKey` rows.
|
||||
- Supply it as a `${secret:}` reference or an environment variable. Never commit it.
|
||||
- Everything else is optional and has defaults:
|
||||
- `OpcUa:CredentialLockout:*` (#833/#844/#845; UserName/API-key only, never Anonymous).
|
||||
- `Security:ApiKeys` miss budgets, `StoreTimeoutMs` and `StoreCacheSeconds`.
|
||||
- `DriverWrites:Timeout` (10 s, #777).
|
||||
- `ServerHistorian:EventReadHorizon` (`00:00:00` = off, #694).
|
||||
- `ServerHistorian:Gateway:*`.
|
||||
- `Scripting:Runner`: the default stays `InProcess`. #793 says not to enable `Shadow` or `OutOfProcess` on the
|
||||
wonder boxes yet.
|
||||
- Out-of-range values for these log a Warning or fail start only when they are set (`docs/Configuration.md`,
|
||||
`docs/security.md`).
|
||||
|
||||
**Deployed appsettings vs master.** The deployed files were diffed against the `b8b4c18f` and `master`
|
||||
copies, with secrets redacted.
|
||||
|
||||
- `appsettings.json`:
|
||||
- `ContinuousHistorization` still carries the pre-b8b4c18f values (`Capacity 0`, `DrainBatchSize 64`; no
|
||||
`DrainConcurrency`, `MaxWriteAttempts` or `CaptureWhilePrimaryUnreachable`). Master's in-code defaults fill
|
||||
the absent keys, and the box's explicit values win. This only matters if continuous historization is
|
||||
enabled.
|
||||
- `DriverWrites` and `EventReadHorizon` are absent, so the defaults apply.
|
||||
- The box uses `Secrets:MasterKey` `FilePath` instead of the shipped `EnvVarName`. That is a site choice and is
|
||||
unchanged.
|
||||
- `appsettings.driver.json`: master lowers `Opc.Ua` from Debug to Information. The fused node loads the
|
||||
`admin-driver` overlay instead, which is unchanged.
|
||||
- **Keep the box's own `appsettings*.json` across the swap.** Do not take the publisher's copies.
|
||||
|
||||
**Other host-level changes:**
|
||||
- The publish output now contains `script-worker/` (about 97 MB). It is unused while the runner is `InProcess`
|
||||
(`docs/ServiceHosting.md` §Script worker processes).
|
||||
- `/health/ready` gains three checks:
|
||||
- OPC UA endpoint started;
|
||||
- script runner (at worst Degraded);
|
||||
- deploy coordinator (at worst Degraded).
|
||||
|
||||
Source: `Host/Health/HealthEndpoints.cs`.
|
||||
- No new ports are opened.
|
||||
|
||||
## (c) PKI and OPC UA SDK 2.0.1 (#782)
|
||||
|
||||
- **SDK version.** It moves from 1.5.378.106 to 2.0.1 (`Directory.Packages.props`), passing through
|
||||
1.5.378.182 at `2da58d03`. Namespace URIs and NodeIds are unchanged.
|
||||
- **Certificates and policies.** The PKI store is still the Directory store under `OpcUa:PkiStoreRoot` (default
|
||||
`pki`, beside the exe). The existing application certificate is reused with the same thumbprint and private
|
||||
key. Security profiles are unchanged: `None`, `Basic256Sha256Sign` and `Basic256Sha256SignAndEncrypt`.
|
||||
Sources: `docs/security.md`, `docs/plans/2026-11-10-opcua-sdk-2-upgrade.md`.
|
||||
- **CA KeyUsage.** #793 says CA certificates must carry the `keyCertSign`/`cRLSign` KeyUsage, or SDK
|
||||
1.5.378.182+ rejects them. The upgrade plan (line 159) lists it as a preflight check. **Before the swap, check
|
||||
every CA in `pki\issuer` and `pki\trusted`, and ScadaBridge's trust of this server's chain.** A self-signed
|
||||
application certificate is not a CA and is not affected. The repo states no further KeyUsage rule.
|
||||
- **Release status.** `docs/plans/2026-11-10-opcua-sdk-2-rollout.md` still marks the release **HOLD**. Its open
|
||||
gates are the memory soak (#799), Windows service validation (#800) and live consumer testing (#801). vd03
|
||||
would be the first Windows host to run SDK 2. Record the go/hold decision in #782.
|
||||
- **ScadaBridge.** The adapter passed its 18 tests on `None` + API key. SignAndEncrypt from ScadaBridge is still
|
||||
untested because ScadaBridge has no client certificate yet (ScadaBridge#121; the issue predates this upgrade).
|
||||
|
||||
## (d) What ScadaBridge will see (b8b4c18f..master)
|
||||
|
||||
The detail is in `../scadaproj/.claude/skills/sister-project-history/otopcua.md`; the commits are cited below.
|
||||
|
||||
**On this FOCAS box:**
|
||||
- **Macros.** Macros are now decoded at the right offset (`665ecd55`), so `parts-count`/`parts-required`
|
||||
(`MACRO:3901`/`3902`) change magnitude. A vacant macro reads `BadNoData`. A fractional value on an integer tag
|
||||
reads `BadTypeMismatch`.
|
||||
- **FixedTree.**
|
||||
- Positions are scaled by their envelope (`69e0a622`; for example 2801574 → 280.1574).
|
||||
- Servo/spindle `Load` is now a real percent, and spindle Load is Float64 (`1dba46bd`).
|
||||
- `Spindle/*/MaxRpm` is renamed `MotorSpeedPct` (`1dba46bd`).
|
||||
- `Axes/AA_1`/`AA_2` are renamed `AA6`/`AA7` (`9460da51`).
|
||||
- Unreported program fields read `BadNoData` instead of a Good 0 (`26b512ad`).
|
||||
- A CNC refusal makes only that tag Bad and no longer Degrades the driver (`2724d7da`, #740).
|
||||
- `String` on a PMC/PARAM/MACRO address is refused, and the tag is skipped (`cc140593`).
|
||||
- New nodes: a `Status/` subtree (#257) and optional `ALARM:` conditions (#647).
|
||||
- **ServiceLevel** drops to 0 if the OPC UA endpoint fails to start (#762).
|
||||
- **Virtual tags.** A disabled virtual tag reads `BadOutOfService` (#787). VT→VT reads now produce values (#791).
|
||||
- **Logins and API keys.**
|
||||
- Credential lockout now covers UserName/API-key logins only, never Anonymous (#833/#844/#845).
|
||||
- Unknown API keys are rate-limited per client (#751).
|
||||
- Expired or disabled keys are refused for writes and acks (#563).
|
||||
- **ACL.** Event and subscription filtering by ACL changes only if `Security:Acl:Enabled` is on (#586/#588,
|
||||
#797/#808, #826). Whether it is on for vd03 was not determined.
|
||||
- **Address-space changes.** Surgically added raw nodes are now announced with model-change events (#599).
|
||||
Native condition state survives a rebuild (#641), and the native alarm feed survives a deploy (#662).
|
||||
- **Alarms and history.** Each method transition emits exactly one event (#587). Overload deferral is per
|
||||
condition (#838/#854/#855). Event history now carries the event type and condition name (#746). Processed
|
||||
reads with no historian return `BadAggregateNotSupported` (#682).
|
||||
|
||||
**Not reachable here (no such driver):**
|
||||
- Galaxy: OPC DA quality grading of status rows (#866, `363f0d81`), first-value race (#859), acks wait for
|
||||
confirmation (#869).
|
||||
- MTConnect: blank string reads Good `""` (#824).
|
||||
- OpcUaClient: host-name check (#816).
|
||||
- Heidenhain: new driver (#803).
|
||||
- AbCip: the native transport is the default.
|
||||
|
||||
## (e) Install, swap and rollback
|
||||
|
||||
The source is `docs/plans/2026-11-10-opcua-sdk-2-rollout.md` (§Sequence, §Rollback). `docs/ServiceHosting.md`
|
||||
§Install/Uninstall describes a fresh install only.
|
||||
|
||||
1. **Publish** a fresh directory:
|
||||
`dotnet publish src/Server/ZB.MOM.WW.OtOpcUa.Host/ZB.MOM.WW.OtOpcUa.Host.csproj -c Release -r win-x64 --self-contained true -o <dir>`.
|
||||
Hash it into a manifest stored outside the directory, then copy it to the box over SMB.
|
||||
2. **Back up** these:
|
||||
- the whole install, with the service stopped: `robocopy E:\ApiInstall\OtOpcUa <backup> /E /COPYALL /DCOPY:DAT`.
|
||||
This covers `pki\`, `appsettings*.json`, `data\` (LocalDb) and `logs\`;
|
||||
- the secrets SQLite file at `Secrets__SqlitePath` (it is outside the install), and its master-key file;
|
||||
- `reg export HKLM\SYSTEM\CurrentControlSet\Services\OtOpcUaHost`. **It holds secrets, so protect the file;**
|
||||
- `BACKUP DATABASE OtOpcUaConfig`.
|
||||
3. **Add the pepper** (action 2) and check the JWT key length (action 3) on the service key. Apply the migration
|
||||
(§a).
|
||||
4. **Swap the directories:**
|
||||
- `Stop-Service OtOpcUaHost` and wait for Stopped.
|
||||
- `Move-Item` the live install to a fresh sibling `$PreviousInstall`, then `Move-Item` the candidate into
|
||||
`E:\ApiInstall\OtOpcUa`.
|
||||
- Copy back the box's `appsettings*.json`, `pki\` and `data\`.
|
||||
- `Start-Service` and wait for Running.
|
||||
5. **Never run `Install-Services.ps1`, `Uninstall-Services.ps1` or `Refresh-Services.ps1` here.**
|
||||
- `sc delete` wipes the 23 environment variables.
|
||||
- `Install-Services.ps1` rewrites the `Environment` value with only 2–3 entries.
|
||||
- `Refresh-Services.ps1` assumes NSSM and `C:\publish`.
|
||||
- The folder swap leaves the registry key untouched.
|
||||
6. **Rollback.**
|
||||
- Stop the service and move the candidate to a quarantine path.
|
||||
- Restore `$PreviousInstall` (or the robocopy backup), including its `appsettings*.json`, `pki\` and `data\`.
|
||||
- Check the certificate thumbprint, then start the service.
|
||||
- Clients reconnect. Do not replay writes or acks.
|
||||
|
||||
No schema undo is needed (§a). Remove the pepper only if it got in the way; `b8b4c18f` ignores it. The
|
||||
baseline already reads gzip artifacts (`9dbd5e96` predates `b8b4c18f`), so artifacts written by master stay
|
||||
readable after a rollback.
|
||||
|
||||
**Not determined:** whether the `b8b4c18f` build reads a LocalDb or alarm-outbox file after master has
|
||||
written to it. Restore `data\` from the backup.
|
||||
|
||||
## (f) Post-deploy checks
|
||||
|
||||
- **Boot log** `logs\otopcua-YYYYMMDD.log`. Read its contents; the listed size shows 0 while the service holds
|
||||
it open.
|
||||
- Expect `OtOpcUa.Host starting with roles=[admin,driver]`, with no roles-disagreement warning.
|
||||
- About 15 s later expect `Credential-reference preflight (lmxopcua#815): every stored and deployed reference is namespaced.`
|
||||
This comes from `CredentialReferencePreflightService` in `AdminUI/Deployments/CredentialReferencePreflight.cs`.
|
||||
- If the service stops immediately, check the Windows Event Log and stderr for the pepper/JWT `FATAL:` message
|
||||
(`Program.cs`, #618).
|
||||
- **Health**, at `http://wonder-app-vd03.zmr.zimmer.com:9000`:
|
||||
- `/healthz`: process up.
|
||||
- `/health/ready`: ConfigDb, Akka Up, OPC UA endpoint started, script runner, deploy coordinator.
|
||||
- `/health/active`.
|
||||
|
||||
All three should return 200 on this single node (`Host/Health/HealthEndpoints.cs`, `docs/ServiceHosting.md`
|
||||
§Health endpoints).
|
||||
- **`/raw` credential banner (#834, warn-only by owner decision).** It should be absent. FOCAS has no credential
|
||||
fields (`docs/security.md` §"Credential references are namespaced per driver"). A banner listing rows means a
|
||||
stored, deployed or running out-of-namespace reference that the next deploy will refuse.
|
||||
- **`/hosts`.** The FOCAS driver row should be Healthy, with the per-node chips (#847/#851). Galaxy host statuses
|
||||
do not apply here (no Galaxy driver).
|
||||
- **Deploy once from the AdminUI**, then check `/fleet` and `/deployments`.
|
||||
- Expect `Applied`, not `Applied (degraded)` (#601, `docs/Redundancy.md`). Node rows show `Applying` until the
|
||||
address-space outcome is reported (#868).
|
||||
- A 3952 error means §a was skipped.
|
||||
- A `VirtualTagNoEffectiveTrigger`, `CredentialReferenceOutOfScope` or similar rejection means a preflight row
|
||||
was missed.
|
||||
- **OPC UA** at `opc.tcp://wonder-app-vd03.zmr.zimmer.com:4840/OtOpcUa`:
|
||||
- The server certificate thumbprint is unchanged (`A873DF51…`) and ServiceLevel is non-zero.
|
||||
- Browse the raw and UNS trees and read `parts-count` with the Client CLI (`docs/Client.CLI.md`). Expect the new
|
||||
magnitude.
|
||||
- From ScadaBridge: it reconnects, values are Good, and any `MaxRpm`/`AA_x`/`Load` bindings have been
|
||||
re-pointed.
|
||||
|
||||
## (g) Preflight SQL: `scripts/preflight/vd03-preupgrade-b8b4c18f-configdb-checks.sql`
|
||||
|
||||
**What it is.** The script is read-only: it uses SELECT plus table variables. It is written against the
|
||||
`b8b4c18f` schema and needs SQL Server 2016+ at compatibility level 130 or higher.
|
||||
|
||||
**How to run it:**
|
||||
`sqlcmd -S <instance> -d OtOpcUaConfig -E -i vd03-preupgrade-b8b4c18f-configdb-checks.sql -W -s "|" -o preflight.txt`
|
||||
|
||||
**Output.**
|
||||
- Each check produces a header row (`ok` / `attention` / `info`, the row count, whether it blocks the deploy,
|
||||
and its source), followed by its detail rows.
|
||||
- Credential values are never printed: reference names are shown, and literals are redacted.
|
||||
|
||||
| # | Check | Blocks deploy? | Enforced on master by | Source |
|
||||
|---|---|---|---|---|
|
||||
| 0 | Environment: database, compatibility level, last migration, snapshot-isolation state | — | — | — |
|
||||
| 1 | `VirtualTagNoEffectiveTrigger` | yes | `DraftValidator.ValidateVirtualTagTriggers` | #783 `d9b95dcf` |
|
||||
| 2 | `CalculationNoEffectiveTrigger` | yes | `DraftValidator.ValidateCalculationTags` rule 3 | `4d3fc9c8` |
|
||||
| 3 | Scripts that read a VirtualTag: they now resolve and start producing values | no (behaviour) | `VirtualTagHostActor` via dependency mux | #791 `000d7f55` |
|
||||
| 4 | `VirtualTagDependencyCycle` (approximate: bounded search, 25 hops) | yes | `DraftValidator.ValidateVirtualTagDependencyCycles` | #791 `000d7f55` |
|
||||
| 5 | Newly forbidden script APIs (#793 list plus E2's `System.Threading`, `Type` reflection, `RuntimeHelpers`) | no (compile failure at runtime → node Bad) | `Core.Scripting/ForbiddenTypeAnalyzer.cs` | #792 `0e96dd08`, E2 `2e916031` |
|
||||
| 6 | `CredentialReferenceOutOfScope`, plus the #836 `UserCertificatePath` rule | yes | `CredentialReferenceDeployChecks`, `DriverCredentialPolicy` | #815 `a224fd63`, #836 `63331b04`, #850 `3dbfe6ec` |
|
||||
| 7 | `CredentialReferenceOtherInstance` (`secret:` segment / `env:` part names another same-type driver) | yes | `DriverCredentialPolicy.DescribeOtherInstanceProblems` | #835 `63331b04`, #849 `4a6229e4`/`d68dff09` |
|
||||
| 8 | Reserved driver names (`drv`, `DRV-<12 hex>`) | no (save/rename refused) | `DriverCredentialPolicy.IsReservedInstanceName` | #848/#849 `d08e59ee`/`d68dff09` |
|
||||
| 9 | `MTConnectMultipleDevices` | yes | `DraftValidator.ValidateMTConnectSingleDevice` | #628 `dcc827ca` |
|
||||
| 10 | Sql: `SqlQueryNotDeclared` (10a), the `write` block (10b, review), writable mismatch (10c, warning) | 10a/10b yes | `SqlTagDeployChecks` | #781 `241318e7`, `f5c30ed0` |
|
||||
| 11 | Heidenhain drivers present (expected none; the type is new) | yes | `HeidenhainDeployChecks` | #803 `2b45694f` |
|
||||
| 12 | UNS effective-name collisions (an unchanged rule, re-checked) | yes | `DraftValidator.ValidateUnsEffectiveLeafUniqueness` | — |
|
||||
| 13 | FOCAS renames, `String` refusals and value changes | no (deploy warning; client-visible) | `FocasTagDefinitionFactory`, FOCAS wire client | `1dba46bd`, `9460da51`, `cc140593`, `69e0a622`, `665ecd55`, `26b512ad` |
|
||||
| 14 | Inventory: drivers, tags, VTs, scripts, API keys | — | — | — |
|
||||
|
||||
**Approximations.** T-SQL cannot reproduce the C# regexes or `System.Text.Json` exactly; the script header lists
|
||||
every approximation. In short:
|
||||
- `ctx.GetTag("…")` literals are extracted after whitespace normalisation.
|
||||
- Cycles are found by a bounded path search.
|
||||
- `ISJSON` is stricter than `System.Text.Json` (comments, trailing commas).
|
||||
- The #850 merged driver+device path check is shown as `review`.
|
||||
- Check 5 is a text search, so its `review` patterns can be false positives (for example
|
||||
`JsonElement.GetProperty`).
|
||||
|
||||
Some branches cannot occur under the database's own CHECK constraints but are kept for completeness. For
|
||||
example, `CK` on `VirtualTag` makes `ChangeTriggered = 0` without a timer impossible, so only the "no literal
|
||||
read" branch of check 1 can fire.
|
||||
|
||||
**Not covered.** `EquipmentTagConfigInspector` findings are warnings only (`Deployment:TagConfigValidationMode=Warn`
|
||||
by default, `AdminOperationsActor`). Cluster topology, ACL scope and `EquipReferenceUnresolved` are unchanged
|
||||
since the baseline.
|
||||
|
||||
**How it was verified (2026-11-11):**
|
||||
- A throwaway database `otopcua_vd03preflight_<hex>` was built on the local docker-dev SQL Server 2022
|
||||
(`localhost,14330`) at compatibility level 130. It was migrated with `dotnet ef database update
|
||||
20260731162342_AddApiKeyTable`, which is the `b8b4c18f` schema.
|
||||
- It was seeded with one or more rows that trigger each check. Every check fired as expected, and check 11 was
|
||||
the expected `ok`. The database was then dropped.
|
||||
- The script also ran clean against the docker-dev `OtOpcUa` database (master schema, 53 drivers). Every gate
|
||||
check was `ok` there, which matches master's own gate accepting that config.
|
||||
|
||||
## Not determined
|
||||
|
||||
- Whether the box's `ServerHistorian__Enabled` is true, and if so, which HistorianGateway build it points at
|
||||
(client 1.0.0 compatibility).
|
||||
- Whether the box's JWT signing key is at least 32 bytes.
|
||||
- Whether `Security:Acl:Enabled` is on.
|
||||
- ScadaBridge's security mode against vd03.
|
||||
- Whether `b8b4c18f` can reopen LocalDb or outbox files after master has written them; restore them from backup
|
||||
on rollback.
|
||||
- The current #782 go/hold state. The repo runbook says HOLD.
|
||||
@@ -0,0 +1,680 @@
|
||||
/* =====================================================================================================
|
||||
vd03-preupgrade-b8b4c18f-configdb-checks.sql
|
||||
|
||||
Pre-upgrade ConfigDb checks for wonder-app-vd03: OtOpcUa b8b4c18f (2026-09-03, the build actually deployed —
|
||||
identified by PDB source-hash + deps.json package versions) -> master 363f0d81.
|
||||
Gitea lmxopcua#793. Companion notes: docs/deploy/vd03-upgrade-b8b4c18f-to-master.md
|
||||
|
||||
STRICTLY READ-ONLY. SELECT statements only, plus INSERTs into local TABLE VARIABLES (session-scoped, never
|
||||
written to this database). No DDL, no DML against any database table, no temp tables, no transactions.
|
||||
|
||||
Run it against the ConfigDb while the box is STILL on b8b4c18f, i.e. before the EnableSnapshotIsolation
|
||||
migration and before the new binaries start:
|
||||
|
||||
sqlcmd -S WONDER-APP-VD03 -d OtOpcUaConfig -E -i vd03-preupgrade-b8b4c18f-configdb-checks.sql -W -s "|" -o preflight.txt
|
||||
(or open it in SSMS with the OtOpcUaConfig database selected and run it; use "Results to Text" for one file)
|
||||
|
||||
The database name on vd03 is OtOpcUaConfig (docs/deployments/wonder-app-vd03-makino-z-34184.md); the
|
||||
script does not hard-code it (no USE), so select the right database first. Check 0 prints which one you hit.
|
||||
|
||||
Requirements: SQL Server 2016+ AND database compatibility level >= 130 (OPENJSON). Check 0 reports both.
|
||||
|
||||
Schema: written against the b8b4c18f EF model (last migration 20260731162342_AddApiKeyTable — identical to
|
||||
e82881c1; no migration landed between them). master adds only 20261022102459_EnableSnapshotIsolation (no
|
||||
table/column change), so the script also runs unchanged after the upgrade. Every rule checked here is new in
|
||||
b8b4c18f..master (DraftValidator / ForbiddenTypeAnalyzer were untouched between e82881c1 and b8b4c18f).
|
||||
|
||||
OUTPUT: every check emits two result sets
|
||||
(1) a one-row header: check id, title, status ('ok' | 'attention' | 'info'), row count, blocking?, source
|
||||
(2) the detail rows (empty when ok).
|
||||
'attention' + blocking = YES means master's deploy gate (DraftValidator / AdminOperationsActor) will REFUSE the
|
||||
next deploy until fixed. blocking = NO means a behaviour change or an authoring/runtime effect, not a refusal.
|
||||
|
||||
Secrets: no credential VALUE is ever selected. Credential fields are reported as a classification plus the
|
||||
reference NAME (secret:/env: names are not secrets); literals print as '<literal redacted>'.
|
||||
|
||||
Approximations (the C# rules use Roslyn-free regexes and System.Text.Json; T-SQL cannot reproduce them
|
||||
exactly — every approximation errs toward listing MORE rows, never fewer, except where noted):
|
||||
* ctx.GetTag("...") literal extraction normalises whitespace first (CR/LF/TAB -> space, runs collapsed,
|
||||
spaces around '.', '(' and the quotes removed). A path literal containing a double space would be
|
||||
mangled; a GetTag inside a comment or string is counted (the C# regex counts it too).
|
||||
* Cycle detection (check 4) is a bounded path search (depth <= 25) over the same graph DraftValidator
|
||||
builds; it can miss a cycle longer than 25 hops and reports each cycle once per VirtualTag member.
|
||||
* JSON: ISJSON/OPENJSON reject comments and trailing commas that System.Text.Json (with the policy's
|
||||
options) accepts, so such a blob shows as 'config is not valid JSON' here but may pass the gate.
|
||||
* The #850 merged driver+device check is approximated (see check 6).
|
||||
===================================================================================================== */
|
||||
|
||||
SET NOCOUNT ON;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Shared working sets (table variables)
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
|
||||
DECLARE @Num TABLE (n int PRIMARY KEY);
|
||||
WITH x AS (SELECT 1 AS n UNION ALL SELECT n + 1 FROM x WHERE n < 256)
|
||||
INSERT @Num (n) SELECT n FROM x OPTION (MAXRECURSION 256);
|
||||
|
||||
-- Scripts with a whitespace-normalised copy of the source for ctx.GetTag("...") extraction.
|
||||
DECLARE @Script TABLE (ScriptId nvarchar(64) PRIMARY KEY, Name nvarchar(128), Src nvarchar(max) COLLATE Latin1_General_BIN2);
|
||||
INSERT @Script (ScriptId, Name, Src)
|
||||
SELECT s.ScriptId, s.Name,
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
|
||||
REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(
|
||||
REPLACE(REPLACE(REPLACE(s.SourceCode, CHAR(13), ' '), CHAR(10), ' '), CHAR(9), ' ')
|
||||
, ' ', ' '), ' ', ' '), ' ', ' '), ' ', ' '), ' ', ' ')
|
||||
, ' .', '.'), '. ', '.'), 'GetTag (', 'GetTag('), 'GetTag( ', 'GetTag('), '" )', '")'), ' )', ')'), '( "', '("'), '( ', '(')
|
||||
FROM dbo.Script s
|
||||
WHERE s.ScriptId IS NOT NULL;
|
||||
|
||||
-- Literal ctx.GetTag("<ref>") reads (EquipmentScriptPaths.ExtractDependencyRefs: ctx\s*\.\s*GetTag\s*\(\s*"([^"]+)"\s*\)).
|
||||
DECLARE @Ref TABLE (ScriptId nvarchar(64), Ref nvarchar(4000) COLLATE Latin1_General_BIN2);
|
||||
WITH occ AS (
|
||||
SELECT s.ScriptId, CHARINDEX(N'ctx.GetTag("', s.Src) AS p
|
||||
FROM @Script s WHERE CHARINDEX(N'ctx.GetTag("', s.Src) > 0
|
||||
UNION ALL
|
||||
SELECT o.ScriptId, CHARINDEX(N'ctx.GetTag("', s.Src, o.p + 1)
|
||||
FROM occ o INNER JOIN @Script s ON s.ScriptId = o.ScriptId
|
||||
WHERE CHARINDEX(N'ctx.GetTag("', s.Src, o.p + 1) > 0
|
||||
)
|
||||
INSERT @Ref (ScriptId, Ref)
|
||||
SELECT o.ScriptId, SUBSTRING(s.Src, o.p + 12, e.q - (o.p + 12))
|
||||
FROM occ o
|
||||
INNER JOIN @Script s ON s.ScriptId = o.ScriptId
|
||||
CROSS APPLY (SELECT CHARINDEX(N'"', s.Src, o.p + 12) AS q) e
|
||||
WHERE e.q > o.p + 12 AND SUBSTRING(s.Src, e.q + 1, 1) = N')'
|
||||
OPTION (MAXRECURSION 0);
|
||||
|
||||
-- RawPath of every raw tag: <folder chain>/<DriverName>/<DeviceName>/<tag-group chain>/<TagName> (RawPathResolver).
|
||||
DECLARE @FolderPath TABLE (RawFolderId nvarchar(64) PRIMARY KEY, Path nvarchar(4000));
|
||||
WITH f AS (
|
||||
SELECT RawFolderId, CAST(Name AS nvarchar(4000)) AS Path, 0 AS d FROM dbo.RawFolder WHERE ParentRawFolderId IS NULL AND RawFolderId IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT c.RawFolderId, CAST(f.Path + N'/' + c.Name AS nvarchar(4000)), f.d + 1
|
||||
FROM dbo.RawFolder c INNER JOIN f ON c.ParentRawFolderId = f.RawFolderId WHERE f.d < 256
|
||||
)
|
||||
INSERT @FolderPath SELECT RawFolderId, MIN(Path) FROM f GROUP BY RawFolderId OPTION (MAXRECURSION 300);
|
||||
|
||||
DECLARE @GroupPath TABLE (TagGroupId nvarchar(64) PRIMARY KEY, Path nvarchar(4000));
|
||||
WITH g AS (
|
||||
SELECT TagGroupId, CAST(Name AS nvarchar(4000)) AS Path, 0 AS d FROM dbo.TagGroup WHERE ParentTagGroupId IS NULL AND TagGroupId IS NOT NULL
|
||||
UNION ALL
|
||||
SELECT c.TagGroupId, CAST(g.Path + N'/' + c.Name AS nvarchar(4000)), g.d + 1
|
||||
FROM dbo.TagGroup c INNER JOIN g ON c.ParentTagGroupId = g.TagGroupId WHERE g.d < 256
|
||||
)
|
||||
INSERT @GroupPath SELECT TagGroupId, MIN(Path) FROM g GROUP BY TagGroupId OPTION (MAXRECURSION 300);
|
||||
|
||||
DECLARE @RawTag TABLE (
|
||||
TagId nvarchar(64), Name nvarchar(128),
|
||||
DeviceId nvarchar(64), DriverInstanceId nvarchar(64),
|
||||
DriverName nvarchar(128), DriverType nvarchar(32) COLLATE Latin1_General_BIN2,
|
||||
AccessLevel nvarchar(16), DataType nvarchar(32), TagConfig nvarchar(max), RawPath nvarchar(4000) COLLATE Latin1_General_BIN2);
|
||||
INSERT @RawTag
|
||||
SELECT t.TagId, t.Name, t.DeviceId, di.DriverInstanceId, di.Name, di.DriverType, t.AccessLevel, t.DataType, t.TagConfig,
|
||||
CASE WHEN di.RawFolderId IS NOT NULL AND fp.Path IS NULL THEN NULL -- unresolvable folder: no RawPath
|
||||
WHEN t.TagGroupId IS NOT NULL AND gp.Path IS NULL THEN NULL -- unresolvable group: no RawPath
|
||||
ELSE ISNULL(fp.Path + N'/', N'') + di.Name + N'/' + dv.Name + N'/' + ISNULL(gp.Path + N'/', N'') + t.Name END
|
||||
FROM dbo.Tag t
|
||||
INNER JOIN dbo.Device dv ON dv.DeviceId = t.DeviceId
|
||||
INNER JOIN dbo.DriverInstance di ON di.DriverInstanceId = dv.DriverInstanceId
|
||||
LEFT JOIN @FolderPath fp ON fp.RawFolderId = di.RawFolderId
|
||||
LEFT JOIN @GroupPath gp ON gp.TagGroupId = t.TagGroupId;
|
||||
|
||||
-- Calculation tags: RawPath + scriptId (TagConfig.scriptId, string) + trigger fields.
|
||||
DECLARE @Calc TABLE (TagId nvarchar(64), RawPath nvarchar(4000) COLLATE Latin1_General_BIN2, ScriptId nvarchar(4000),
|
||||
ChangeTriggered bit, TimerIntervalMs bigint NULL);
|
||||
INSERT @Calc
|
||||
SELECT r.TagId, r.RawPath,
|
||||
(SELECT TOP 1 j.[value] FROM OPENJSON(CASE WHEN ISJSON(r.TagConfig) = 1 THEN r.TagConfig END) j WHERE j.[key] COLLATE Latin1_General_BIN2 = N'scriptId' AND j.[type] = 1),
|
||||
ISNULL((SELECT TOP 1 CASE j.[value] WHEN N'false' THEN 0 ELSE 1 END FROM OPENJSON(CASE WHEN ISJSON(r.TagConfig) = 1 THEN r.TagConfig END) j
|
||||
WHERE j.[key] COLLATE Latin1_General_BIN2 = N'changeTriggered' AND j.[type] = 3), 1),
|
||||
(SELECT TOP 1 TRY_CAST(j.[value] AS bigint) FROM OPENJSON(CASE WHEN ISJSON(r.TagConfig) = 1 THEN r.TagConfig END) j
|
||||
WHERE j.[key] COLLATE Latin1_General_BIN2 = N'timerIntervalMs' AND j.[type] = 2)
|
||||
FROM @RawTag r WHERE r.DriverType = N'Calculation';
|
||||
|
||||
-- UNS reference effective names -> backing RawPath (EquipmentReferenceMap: DisplayNameOverride else raw tag Name).
|
||||
DECLARE @EquipRef TABLE (EquipmentId nvarchar(64) COLLATE Latin1_General_BIN2, EffName nvarchar(128) COLLATE Latin1_General_BIN2, RawPath nvarchar(4000) COLLATE Latin1_General_BIN2);
|
||||
INSERT @EquipRef
|
||||
SELECT u.EquipmentId, CASE WHEN ISNULL(u.DisplayNameOverride, N'') = N'' THEN r.Name ELSE u.DisplayNameOverride END, r.RawPath
|
||||
FROM dbo.UnsTagReference u INNER JOIN @RawTag r ON r.TagId = u.TagId;
|
||||
|
||||
-- Enabled VirtualTags and their mux key {EquipmentId}/{Name}.
|
||||
DECLARE @VT TABLE (VirtualTagId nvarchar(64), EquipmentId nvarchar(64) COLLATE Latin1_General_BIN2, Name nvarchar(128) COLLATE Latin1_General_BIN2,
|
||||
VtKey nvarchar(300) COLLATE Latin1_General_BIN2, ScriptId nvarchar(64),
|
||||
ChangeTriggered bit, TimerIntervalMs int NULL, Enabled bit);
|
||||
INSERT @VT SELECT v.VirtualTagId, v.EquipmentId, v.Name, v.EquipmentId + N'/' + v.Name, v.ScriptId, v.ChangeTriggered, v.TimerIntervalMs, v.Enabled
|
||||
FROM dbo.VirtualTag v;
|
||||
|
||||
-- Which entity uses which script (for reporting).
|
||||
DECLARE @ScriptUse TABLE (ScriptId nvarchar(64), UsedByKind nvarchar(20), UsedById nvarchar(64), UsedByEnabled bit, EquipmentId nvarchar(64) NULL);
|
||||
INSERT @ScriptUse SELECT ScriptId, N'VirtualTag', VirtualTagId, Enabled, EquipmentId FROM @VT;
|
||||
INSERT @ScriptUse SELECT a.PredicateScriptId, N'ScriptedAlarm', a.ScriptedAlarmId, a.Enabled, a.EquipmentId FROM dbo.ScriptedAlarm a;
|
||||
INSERT @ScriptUse SELECT c.ScriptId, N'CalculationTag', c.TagId, 1, NULL FROM @Calc c WHERE c.ScriptId IS NOT NULL;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 0 — environment
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @lastMig nvarchar(150) = (SELECT TOP 1 MigrationId FROM dbo.__EFMigrationsHistory ORDER BY MigrationId DESC);
|
||||
DECLARE @compat int = (SELECT compatibility_level FROM sys.databases WHERE database_id = DB_ID());
|
||||
DECLARE @snap nvarchar(60) = (SELECT snapshot_isolation_state_desc FROM sys.databases WHERE database_id = DB_ID());
|
||||
SELECT N'0' AS check_id, N'Environment' AS title,
|
||||
CASE WHEN @lastMig = N'20260731162342_AddApiKeyTable' AND @compat >= 130 THEN N'ok' ELSE N'attention' END AS status,
|
||||
1 AS row_count, N'NO' AS blocking,
|
||||
N'expects last migration 20260731162342_AddApiKeyTable (b8b4c18f) and compat >= 130' AS source;
|
||||
SELECT DB_NAME() AS database_name, @@SERVERNAME AS server_name, CAST(SERVERPROPERTY('ProductVersion') AS nvarchar(40)) AS sql_version,
|
||||
@compat AS compatibility_level, @lastMig AS last_applied_migration, @snap AS snapshot_isolation_state,
|
||||
N'master needs migration 20261022102459_EnableSnapshotIsolation (ALLOW_SNAPSHOT_ISOLATION ON) before the first deploy' AS note;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 1 — #783 VirtualTagNoEffectiveTrigger (BLOCKING)
|
||||
master: DraftValidator.ValidateVirtualTagTriggers (d9b95dcf). Enabled VT, TimerIntervalMs null/<=0, and either
|
||||
ChangeTriggered = 0, or ChangeTriggered = 1 with a resolvable script that has NO literal ctx.GetTag read.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c1 TABLE (VirtualTagId nvarchar(64), EquipmentId nvarchar(64), Name nvarchar(128), ScriptId nvarchar(64), ChangeTriggered bit, TimerIntervalMs int NULL, literal_reads int, reason nvarchar(200));
|
||||
INSERT @c1
|
||||
SELECT v.VirtualTagId, v.EquipmentId, v.Name, v.ScriptId, v.ChangeTriggered, v.TimerIntervalMs,
|
||||
(SELECT COUNT(*) FROM @Ref r WHERE r.ScriptId = v.ScriptId),
|
||||
CASE WHEN v.ChangeTriggered = 0 THEN N'ChangeTriggered=0 and no TimerIntervalMs'
|
||||
ELSE N'change-triggered but script has no literal ctx.GetTag read, and no TimerIntervalMs' END
|
||||
FROM @VT v
|
||||
WHERE v.Enabled = 1 AND ISNULL(v.TimerIntervalMs, 0) <= 0
|
||||
AND (v.ChangeTriggered = 0
|
||||
OR (EXISTS (SELECT 1 FROM @Script s WHERE s.ScriptId = v.ScriptId) AND NOT EXISTS (SELECT 1 FROM @Ref r WHERE r.ScriptId = v.ScriptId)));
|
||||
SELECT N'1' AS check_id, N'#783 VirtualTagNoEffectiveTrigger' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'DraftValidator.ValidateVirtualTagTriggers; commit d9b95dcf' AS source FROM @c1;
|
||||
SELECT * FROM @c1 ORDER BY EquipmentId, Name;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 2 — CalculationNoEffectiveTrigger (BLOCKING)
|
||||
master: DraftValidator.ValidateCalculationTags rule 3 (4d3fc9c8, E4/R07:U5). Calc tag with no timerIntervalMs > 0
|
||||
and either changeTriggered=false, or change-triggered with a resolvable script that has no literal ctx.GetTag read.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c2 TABLE (TagId nvarchar(64), RawPath nvarchar(4000), ScriptId nvarchar(4000), changeTriggered bit, timerIntervalMs bigint NULL, reason nvarchar(200));
|
||||
INSERT @c2
|
||||
SELECT c.TagId, c.RawPath, c.ScriptId, c.ChangeTriggered, c.TimerIntervalMs,
|
||||
CASE WHEN c.ChangeTriggered = 0 THEN N'changeTriggered=false and no timerIntervalMs'
|
||||
ELSE N'change-triggered but script has no literal ctx.GetTag read, and no timerIntervalMs' END
|
||||
FROM @Calc c
|
||||
WHERE ISNULL(c.TimerIntervalMs, 0) <= 0
|
||||
AND (c.ChangeTriggered = 0
|
||||
OR (EXISTS (SELECT 1 FROM @Script s WHERE s.ScriptId = c.ScriptId) AND NOT EXISTS (SELECT 1 FROM @Ref r WHERE r.ScriptId = c.ScriptId)));
|
||||
SELECT N'2' AS check_id, N'CalculationNoEffectiveTrigger' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'DraftValidator.ValidateCalculationTags (rule 3); commit 4d3fc9c8' AS source FROM @c2;
|
||||
SELECT * FROM @c2 ORDER BY RawPath;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 3 — #791 scripts that read a VirtualTag (BEHAVIOUR CHANGE, not blocking)
|
||||
master routes VirtualTag outputs through the dependency mux (000d7f55), so a ctx.GetTag("<EquipmentId>/<VtName>")
|
||||
read now RESOLVES and starts producing values / firing where it produced none before. A bare VT name (no '/')
|
||||
never resolved and still does not; it is listed so it can be fixed. {{equip}}/<Name> resolves references only —
|
||||
naming a VT there is EquipReferenceUnresolved (pre-existing error; master's message now hints the key form).
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c3 TABLE (ScriptId nvarchar(64), ScriptName nvarchar(128), used_by nvarchar(max), read_literal nvarchar(4000), match_kind nvarchar(80), target_vt nvarchar(64), target_enabled bit);
|
||||
INSERT @c3
|
||||
SELECT s.ScriptId, s.Name,
|
||||
STUFF((SELECT N', ' + u.UsedByKind + N':' + u.UsedById + CASE WHEN u.UsedByEnabled = 0 THEN N'(disabled)' ELSE N'' END
|
||||
FROM @ScriptUse u WHERE u.ScriptId = s.ScriptId FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N''),
|
||||
r.Ref,
|
||||
CASE WHEN r.Ref = v.VtKey THEN N'VT key {EquipmentId}/{Name}: now resolves (#791)'
|
||||
ELSE N'bare VT name: never resolved, still does not' END,
|
||||
v.VirtualTagId, v.Enabled
|
||||
FROM @Ref r
|
||||
INNER JOIN @Script s ON s.ScriptId = r.ScriptId
|
||||
INNER JOIN @VT v ON r.Ref = v.VtKey OR (r.Ref = v.Name AND CHARINDEX(N'/', r.Ref) = 0);
|
||||
SELECT N'3' AS check_id, N'#791 scripts reading a VirtualTag (VT->VT now works)' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'NO' AS blocking, N'VirtualTag outputs via dependency mux; commit 000d7f55' AS source FROM @c3;
|
||||
SELECT * FROM @c3 ORDER BY ScriptId, read_literal;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 4 — #791 VirtualTagDependencyCycle (BLOCKING) — APPROXIMATE
|
||||
master: DraftValidator.ValidateVirtualTagDependencyCycles (000d7f55). Nodes = enabled VT keys + every Calculation
|
||||
tag RawPath; edges = literal ctx.GetTag reads that are themselves nodes (VT reads after {{equip}}/<RefName>
|
||||
substitution through the equipment's references). Cycles made only of Calculation tags are the pre-existing
|
||||
CalculationDependencyCycle rule and are not listed. Bounded search (25 hops).
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @Node TABLE (NodeKey nvarchar(4000) COLLATE Latin1_General_BIN2, IsVt bit);
|
||||
INSERT @Node SELECT VtKey, 1 FROM @VT WHERE Enabled = 1;
|
||||
INSERT @Node SELECT RawPath, 0 FROM @Calc WHERE RawPath IS NOT NULL AND RawPath NOT IN (SELECT NodeKey FROM @Node);
|
||||
|
||||
DECLARE @Edge TABLE (FromKey nvarchar(4000) COLLATE Latin1_General_BIN2, ToKey nvarchar(4000) COLLATE Latin1_General_BIN2);
|
||||
-- VT edges ({{equip}}/<RefName> substituted to the backing RawPath when the equipment has that reference).
|
||||
INSERT @Edge
|
||||
SELECT DISTINCT v.VtKey, COALESCE(er.RawPath, r.Ref)
|
||||
FROM @VT v
|
||||
INNER JOIN @Ref r ON r.ScriptId = v.ScriptId
|
||||
LEFT JOIN @EquipRef er ON er.EquipmentId = v.EquipmentId AND LEFT(r.Ref, 10) = N'{{equip}}/' AND er.EffName = SUBSTRING(r.Ref, 11, 4000)
|
||||
WHERE v.Enabled = 1 AND COALESCE(er.RawPath, r.Ref) IN (SELECT NodeKey FROM @Node);
|
||||
-- Calculation edges (no {{equip}} substitution for calc scripts).
|
||||
INSERT @Edge
|
||||
SELECT DISTINCT c.RawPath, r.Ref
|
||||
FROM @Calc c INNER JOIN @Ref r ON r.ScriptId = c.ScriptId
|
||||
WHERE c.RawPath IS NOT NULL AND r.Ref IN (SELECT NodeKey FROM @Node);
|
||||
|
||||
DECLARE @c4 TABLE (start_vt_key nvarchar(4000), cycle_path nvarchar(max), hops int);
|
||||
WITH walk AS (
|
||||
SELECT e.FromKey AS StartKey, e.ToKey AS CurKey, CAST(e.FromKey + N' -> ' + e.ToKey AS nvarchar(max)) AS Path,
|
||||
CAST(N'|' + e.FromKey + N'|' AS nvarchar(max)) AS Seen, 1 AS hops
|
||||
FROM @Edge e INNER JOIN @Node n ON n.NodeKey = e.FromKey AND n.IsVt = 1
|
||||
UNION ALL
|
||||
SELECT w.StartKey, e.ToKey, w.Path + N' -> ' + e.ToKey, w.Seen + w.CurKey + N'|', w.hops + 1
|
||||
FROM walk w INNER JOIN @Edge e ON e.FromKey = w.CurKey
|
||||
WHERE w.hops < 25 AND w.CurKey <> w.StartKey AND CHARINDEX(N'|' + w.CurKey + N'|', w.Seen) = 0
|
||||
)
|
||||
INSERT @c4 SELECT StartKey, Path, hops FROM walk WHERE CurKey = StartKey OPTION (MAXRECURSION 30);
|
||||
SELECT N'4' AS check_id, N'#791 VirtualTagDependencyCycle (approximate)' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'DraftValidator.ValidateVirtualTagDependencyCycles; commit 000d7f55' AS source FROM @c4;
|
||||
SELECT * FROM @c4 ORDER BY start_vt_key;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 5 — #792 + E2 newly forbidden script APIs (RUNTIME compile failure, not a deploy refusal)
|
||||
master: Core.Scripting/ForbiddenTypeAnalyzer.cs (0e96dd08 #792; 2e916031 E2). ScriptEvaluator throws
|
||||
ScriptSandboxViolationException at compile, so the VT / Calculation tag / scripted alarm never evaluates (and the
|
||||
/scripts editor shows the diagnostic). Text search over the raw source — case-sensitive, may over-report
|
||||
(e.g. a word in a comment); patterns flagged 'review' are known to collide with legitimate APIs.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @Pat TABLE (pattern nvarchar(100) COLLATE Latin1_General_BIN2, denied nvarchar(200), source nvarchar(40), certainty nvarchar(10));
|
||||
INSERT @Pat VALUES
|
||||
(N'%AppContext%', N'System.AppContext', N'#792 0e96dd08', N'likely'),
|
||||
(N'%Console.%', N'System.Console', N'#792 0e96dd08', N'likely'),
|
||||
(N'%GCSettings%', N'System.Runtime.GCSettings', N'#792 0e96dd08', N'likely'),
|
||||
(N'%ProfileOptimization%', N'System.Runtime.ProfileOptimization', N'#792 0e96dd08', N'likely'),
|
||||
(N'%EncodingProvider%', N'System.Text.(CodePages)EncodingProvider', N'#792 0e96dd08', N'likely'),
|
||||
(N'%TypeDescriptor%', N'System.ComponentModel.TypeDescriptor', N'#792 0e96dd08', N'likely'),
|
||||
(N'%CryptoConfig%', N'System.Security.Cryptography.CryptoConfig', N'#792 0e96dd08', N'likely'),
|
||||
(N'%CurrentCulture%', N'CultureInfo.CurrentCulture / DefaultThreadCurrentCulture', N'#792 0e96dd08', N'likely'),
|
||||
(N'%CurrentUICulture%', N'CultureInfo.CurrentUICulture / DefaultThreadCurrentUICulture', N'#792 0e96dd08', N'likely'),
|
||||
(N'%ClearCachedData%', N'TimeZoneInfo.ClearCachedData', N'#792 0e96dd08', N'likely'),
|
||||
(N'%RuntimeHelpers%', N'System.Runtime.CompilerServices.RuntimeHelpers', N'E2 2e916031', N'likely'),
|
||||
(N'%System.Threading%', N'System.Threading namespace (all but CancellationToken/Interlocked/Volatile)', N'E2 2e916031', N'likely'),
|
||||
(N'%SemaphoreSlim%', N'System.Threading.SemaphoreSlim', N'E2 2e916031', N'likely'),
|
||||
(N'%Monitor.%', N'System.Threading.Monitor', N'E2 2e916031', N'review'),
|
||||
(N'%SpinWait%', N'System.Threading.SpinWait', N'E2 2e916031', N'likely'),
|
||||
(N'%SpinLock%', N'System.Threading.SpinLock', N'E2 2e916031', N'likely'),
|
||||
(N'%ResetEvent%', N'System.Threading.Manual/AutoResetEvent(Slim)', N'E2 2e916031', N'likely'),
|
||||
(N'%Mutex%', N'System.Threading.Mutex', N'E2 2e916031', N'likely'),
|
||||
(N'%WaitHandle%', N'System.Threading.(Event)WaitHandle', N'E2 2e916031', N'likely'),
|
||||
(N'%CancellationTokenSource%', N'System.Threading.CancellationTokenSource', N'E2 2e916031', N'likely'),
|
||||
(N'%ReaderWriterLock%', N'System.Threading.ReaderWriterLock(Slim)', N'E2 2e916031', N'likely'),
|
||||
(N'%ThreadLocal%', N'System.Threading.ThreadLocal', N'E2 2e916031', N'likely'),
|
||||
(N'%AsyncLocal%', N'System.Threading.AsyncLocal', N'E2 2e916031', N'likely'),
|
||||
(N'%CountdownEvent%', N'System.Threading.CountdownEvent', N'E2 2e916031', N'likely'),
|
||||
(N'%Barrier%', N'System.Threading.Barrier', N'E2 2e916031', N'review'),
|
||||
(N'%Thread.%', N'System.Threading.Thread (already denied pre-E2)', N'E2 2e916031', N'review'),
|
||||
(N'%Type.GetType(%', N'System.Type.GetType (reflection entry point)', N'E2 2e916031', N'likely'),
|
||||
(N'%.InvokeMember(%', N'System.Type.InvokeMember', N'E2 2e916031', N'likely'),
|
||||
(N'%.GetMethod%(%', N'System.Type.GetMethod(s)', N'E2 2e916031', N'review'),
|
||||
(N'%.GetPropert%(%', N'System.Type.GetProperty/GetProperties (JsonElement.GetProperty is FINE)', N'E2 2e916031', N'review'),
|
||||
(N'%.GetField%(%', N'System.Type.GetField(s)', N'E2 2e916031', N'review'),
|
||||
(N'%.GetMember%(%', N'System.Type.GetMember(s)', N'E2 2e916031', N'review'),
|
||||
(N'%.GetConstructor%(%', N'System.Type.GetConstructor(s)', N'E2 2e916031', N'review'),
|
||||
(N'%.GetEvent%(%', N'System.Type.GetEvent(s)', N'E2 2e916031', N'review'),
|
||||
(N'%.GetDefaultMembers(%', N'System.Type.GetDefaultMembers', N'E2 2e916031', N'likely');
|
||||
|
||||
DECLARE @c5 TABLE (ScriptId nvarchar(64), ScriptName nvarchar(128), used_by nvarchar(max), denied_api nvarchar(200), certainty nvarchar(10), source nvarchar(40));
|
||||
INSERT @c5
|
||||
SELECT s.ScriptId, s.Name,
|
||||
STUFF((SELECT N', ' + u.UsedByKind + N':' + u.UsedById + CASE WHEN u.UsedByEnabled = 0 THEN N'(disabled)' ELSE N'' END
|
||||
FROM @ScriptUse u WHERE u.ScriptId = s.ScriptId FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N''),
|
||||
p.denied, p.certainty, p.source
|
||||
FROM dbo.Script s
|
||||
INNER JOIN @Pat p ON s.SourceCode COLLATE Latin1_General_BIN2 LIKE p.pattern
|
||||
WHERE s.ScriptId IS NOT NULL;
|
||||
SELECT N'5' AS check_id, N'#792/E2 newly forbidden script APIs' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'NO (fails at compile/run)' AS blocking, N'ForbiddenTypeAnalyzer; commits 0e96dd08, 2e916031' AS source FROM @c5;
|
||||
SELECT * FROM @c5 ORDER BY certainty, ScriptId, denied_api;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Credential-field walk shared by checks 6-7 (#815/#835/#849/#836).
|
||||
Policy (Core.Abstractions/DriverCredentialPolicy.cs + CredentialReferences.cs), fields matched at ANY depth,
|
||||
key compared case-insensitively, in DriverConfig AND every DeviceConfig of the driver:
|
||||
GalaxyMxGateway: ApiKeySecretRef (literal allowed), FallbackPasswordSecretRef (literal only as dev:...)
|
||||
secret:galaxy/... | secret:otopcua/galaxy/... | env:OTOPCUA_GALAXY_... | env:GALAXY_MXGW_...
|
||||
OpcUaClient: Password, UserCertificatePassword (literal allowed); path field UserCertificatePath (#836)
|
||||
secret:opcuaclient/... | env:OTOPCUA_OPCUACLIENT_...
|
||||
Heidenhain: InspectPassword, DncPassword, DataPassword (literal refused) — type did not exist at b8b4c18f.
|
||||
${secret:name} is the same as secret:name. file: is always refused. A prefix in another casing is refused.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @Field TABLE (DriverType nvarchar(32), FieldName nvarchar(64), LiteralPolicy nvarchar(16));
|
||||
INSERT @Field VALUES
|
||||
(N'GalaxyMxGateway', N'ApiKeySecretRef', N'Allowed'), (N'GalaxyMxGateway', N'FallbackPasswordSecretRef', N'DevPrefixOnly'),
|
||||
(N'OpcUaClient', N'Password', N'Allowed'), (N'OpcUaClient', N'UserCertificatePassword', N'Allowed'),
|
||||
(N'Heidenhain', N'InspectPassword', N'Refused'), (N'Heidenhain', N'DncPassword', N'Refused'), (N'Heidenhain', N'DataPassword', N'Refused');
|
||||
DECLARE @Ns TABLE (DriverType nvarchar(32), Kind nvarchar(10), Prefix nvarchar(64) COLLATE Latin1_General_BIN2);
|
||||
INSERT @Ns VALUES
|
||||
(N'GalaxyMxGateway', N'secret', N'galaxy/'), (N'GalaxyMxGateway', N'secret', N'otopcua/galaxy/'),
|
||||
(N'GalaxyMxGateway', N'env', N'OTOPCUA_GALAXY_'), (N'GalaxyMxGateway', N'env', N'GALAXY_MXGW_'),
|
||||
(N'OpcUaClient', N'secret', N'opcuaclient/'), (N'OpcUaClient', N'env', N'OTOPCUA_OPCUACLIENT_'),
|
||||
(N'Heidenhain', N'secret', N'heidenhain/'), (N'Heidenhain', N'env', N'OTOPCUA_HEIDENHAIN_');
|
||||
|
||||
-- Credential-bearing config blobs: DriverConfig + each DeviceConfig.
|
||||
DECLARE @Blob TABLE (DriverInstanceId nvarchar(64), DriverName nvarchar(128), DriverType nvarchar(32), BlobKind nvarchar(30), OwnerId nvarchar(64), OwnerName nvarchar(128), Config nvarchar(max));
|
||||
INSERT @Blob
|
||||
SELECT d.DriverInstanceId, d.Name, ft.DriverType, N'DriverConfig', d.DriverInstanceId, d.Name, d.DriverConfig
|
||||
FROM dbo.DriverInstance d INNER JOIN (SELECT DISTINCT DriverType FROM @Field) ft ON ft.DriverType = d.DriverType -- table is case-insensitive like the C# policy
|
||||
UNION ALL
|
||||
SELECT d.DriverInstanceId, d.Name, ft.DriverType, N'DeviceConfig', v.DeviceId, v.Name, v.DeviceConfig
|
||||
FROM dbo.DriverInstance d INNER JOIN (SELECT DISTINCT DriverType FROM @Field) ft ON ft.DriverType = d.DriverType
|
||||
INNER JOIN dbo.Device v ON v.DriverInstanceId = d.DriverInstanceId;
|
||||
|
||||
-- Recursive JSON walk: every property at every depth, with its parent object (for the #836 AuthType switch).
|
||||
DECLARE @Prop TABLE (DriverInstanceId nvarchar(64), DriverName nvarchar(128), DriverType nvarchar(32), BlobKind nvarchar(30), OwnerId nvarchar(64), OwnerName nvarchar(128),
|
||||
FieldPath nvarchar(4000), PropKey nvarchar(4000), PropValue nvarchar(max) COLLATE Latin1_General_BIN2, PropType tinyint, ParentJson nvarchar(max));
|
||||
WITH j AS (
|
||||
SELECT b.DriverInstanceId, b.DriverName, b.DriverType, b.BlobKind, b.OwnerId, b.OwnerName,
|
||||
CAST(o.[key] AS nvarchar(4000)) AS FieldPath, CAST(o.[key] AS nvarchar(4000)) AS PropKey, o.[value] AS PropValue, CAST(o.[type] AS tinyint) AS PropType,
|
||||
CAST(b.Config AS nvarchar(max)) AS ParentJson, 0 AS depth, CAST(0 AS bit) AS InArray
|
||||
FROM @Blob b CROSS APPLY OPENJSON(CASE WHEN ISJSON(b.Config) = 1 THEN b.Config END) o
|
||||
UNION ALL
|
||||
SELECT j.DriverInstanceId, j.DriverName, j.DriverType, j.BlobKind, j.OwnerId, j.OwnerName,
|
||||
CAST(CASE WHEN j.PropType = 4 THEN j.FieldPath + N'[' + c.[key] + N']' ELSE j.FieldPath + N'.' + c.[key] END AS nvarchar(4000)),
|
||||
CAST(c.[key] AS nvarchar(4000)), c.[value], CAST(c.[type] AS tinyint), CAST(j.PropValue AS nvarchar(max)), j.depth + 1,
|
||||
CAST(CASE WHEN j.PropType = 4 THEN 1 ELSE 0 END AS bit)
|
||||
FROM j CROSS APPLY OPENJSON(j.PropValue) c
|
||||
WHERE j.PropType IN (4, 5) AND j.depth < 32
|
||||
)
|
||||
INSERT @Prop SELECT DriverInstanceId, DriverName, DriverType, BlobKind, OwnerId, OwnerName, FieldPath, PropKey, PropValue, PropType, ParentJson
|
||||
FROM j WHERE InArray = 0 OPTION (MAXRECURSION 40);
|
||||
|
||||
-- Classify every credential field value (CredentialReferences.Parse + IsAcceptable).
|
||||
DECLARE @Cred TABLE (DriverInstanceId nvarchar(64), DriverName nvarchar(128), DriverType nvarchar(32), BlobKind nvarchar(30), OwnerId nvarchar(64), OwnerName nvarchar(128),
|
||||
FieldPath nvarchar(4000), LiteralPolicy nvarchar(16), Kind nvarchar(80), RefName nvarchar(4000) COLLATE Latin1_General_BIN2, Acceptable bit, Shown nvarchar(4000));
|
||||
INSERT @Cred
|
||||
SELECT p.DriverInstanceId, p.DriverName, p.DriverType, p.BlobKind, p.OwnerId, p.OwnerName, p.FieldPath, f.LiteralPolicy, k.Kind, k.RefName,
|
||||
CASE WHEN k.Kind IN (N'blank', N'secret', N'env') THEN 1
|
||||
WHEN k.Kind = N'literal' AND f.LiteralPolicy = N'Allowed' THEN 1
|
||||
WHEN k.Kind = N'literal' AND f.LiteralPolicy = N'DevPrefixOnly' AND LOWER(LEFT(p.PropValue, 4)) = N'dev:' THEN 1
|
||||
ELSE 0 END,
|
||||
CASE WHEN k.Kind IN (N'literal') THEN N'<literal redacted>' WHEN k.Kind = N'non-string' THEN N'<non-string JSON value>'
|
||||
WHEN LOWER(LEFT(p.PropValue, 4)) = N'dev:' THEN N'dev:<redacted>' ELSE p.PropValue END
|
||||
FROM @Prop p
|
||||
INNER JOIN @Field f ON f.DriverType = p.DriverType AND LOWER(f.FieldName) = LOWER(p.PropKey)
|
||||
CROSS APPLY (
|
||||
SELECT
|
||||
CASE WHEN p.PropType = 0 OR (p.PropType = 1 AND p.PropValue = N'') THEN N'blank'
|
||||
WHEN p.PropType <> 1 THEN N'non-string'
|
||||
WHEN (LOWER(LEFT(p.PropValue, 9)) = N'${secret:' AND LEFT(p.PropValue, 9) <> N'${secret:')
|
||||
OR (LOWER(LEFT(p.PropValue, 7)) = N'secret:' AND LEFT(p.PropValue, 7) <> N'secret:')
|
||||
OR (LOWER(LEFT(p.PropValue, 4)) = N'env:' AND LEFT(p.PropValue, 4) <> N'env:') THEN N'out-of-scope (prefix casing)'
|
||||
WHEN LEFT(p.PropValue, 9) = N'${secret:' AND RIGHT(p.PropValue, 1) <> N'}' THEN N'out-of-scope (malformed ${secret:})'
|
||||
WHEN LEFT(p.PropValue, 9) = N'${secret:' OR LEFT(p.PropValue, 7) = N'secret:' THEN N'secret?'
|
||||
WHEN LEFT(p.PropValue, 4) = N'env:' THEN N'env?'
|
||||
WHEN LOWER(LEFT(p.PropValue, 5)) = N'file:' THEN N'out-of-scope (file:)'
|
||||
ELSE N'literal' END AS RawKind,
|
||||
CASE WHEN LEFT(p.PropValue, 9) = N'${secret:' AND RIGHT(p.PropValue, 1) = N'}' THEN SUBSTRING(p.PropValue, 10, LEN(p.PropValue) - 10)
|
||||
WHEN LEFT(p.PropValue, 7) = N'secret:' THEN SUBSTRING(p.PropValue, 8, 4000)
|
||||
WHEN LEFT(p.PropValue, 4) = N'env:' THEN SUBSTRING(p.PropValue, 5, 4000) END AS Name
|
||||
) rk
|
||||
CROSS APPLY (
|
||||
SELECT
|
||||
CASE WHEN rk.RawKind = N'secret?' THEN
|
||||
CASE WHEN EXISTS (SELECT 1 FROM @Ns n WHERE n.DriverType = p.DriverType AND n.Kind = N'secret'
|
||||
AND LEFT(rk.Name, LEN(n.Prefix)) = n.Prefix
|
||||
AND LEN(rk.Name) - LEN(n.Prefix) BETWEEN 1 AND 200
|
||||
AND SUBSTRING(rk.Name, LEN(n.Prefix) + 1, 4000) NOT LIKE N'%[^-A-Za-z0-9._/]%'
|
||||
AND N'/' + SUBSTRING(rk.Name, LEN(n.Prefix) + 1, 4000) + N'/' NOT LIKE N'%//%'
|
||||
AND N'/' + SUBSTRING(rk.Name, LEN(n.Prefix) + 1, 4000) + N'/' NOT LIKE N'%/./%'
|
||||
AND N'/' + SUBSTRING(rk.Name, LEN(n.Prefix) + 1, 4000) + N'/' NOT LIKE N'%/../%')
|
||||
THEN N'secret' ELSE N'out-of-scope (secret: outside namespace)' END
|
||||
WHEN rk.RawKind = N'env?' THEN
|
||||
CASE WHEN EXISTS (SELECT 1 FROM @Ns n WHERE n.DriverType = p.DriverType AND n.Kind = N'env'
|
||||
AND LEFT(rk.Name, LEN(n.Prefix)) = n.Prefix
|
||||
AND LEN(rk.Name) - LEN(n.Prefix) BETWEEN 1 AND 100
|
||||
AND SUBSTRING(rk.Name, LEN(n.Prefix) + 1, 4000) NOT LIKE N'%[^A-Z0-9_]%')
|
||||
THEN N'env' ELSE N'out-of-scope (env: outside prefix)' END
|
||||
ELSE rk.RawKind END AS Kind,
|
||||
rk.Name AS RefName
|
||||
) k;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 6 — #815 CredentialReferenceOutOfScope (+ #836 UserCertificatePath) (BLOCKING)
|
||||
master: Configuration/Validation/CredentialReferenceDeployChecks.cs (a224fd63 #815; 63331b04 #836; 3dbfe6ec #850).
|
||||
A RUNNING driver keeps resolving an old reference warn-only (LegacyCredentialReferenceTracker), but the NEXT deploy
|
||||
is refused until the field is migrated. Same rule the AdminUI /raw banner and CredentialReferencePreflightService
|
||||
report (#834). Also lists config blobs that are not valid JSON (the gate reports those too).
|
||||
UserCertificatePath: must be blank or a relative path of [A-Za-z0-9._-] segments inside
|
||||
Drivers:OpcUaClient:UserCertificateDirectory; only checked where AuthType in the SAME object is absent/null/
|
||||
'Certificate'/2 — rows where AuthType sits in the other blob are 'review' (the gate checks the merged config, #850).
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c6 TABLE (DriverType nvarchar(32), DriverName nvarchar(128), DriverInstanceId nvarchar(64), blob nvarchar(30), owner_name nvarchar(128), field_path nvarchar(4000),
|
||||
finding nvarchar(200), value_shown nvarchar(4000), severity nvarchar(10));
|
||||
INSERT @c6
|
||||
SELECT c.DriverType, c.DriverName, c.DriverInstanceId, c.BlobKind, c.OwnerName, c.FieldPath,
|
||||
CASE WHEN c.Kind = N'literal' AND c.LiteralPolicy = N'DevPrefixOnly' THEN N'literal refused: must be secret:/env: in namespace or explicit dev:VALUE'
|
||||
WHEN c.Kind = N'literal' THEN N'literal refused for this field'
|
||||
ELSE c.Kind END,
|
||||
c.Shown, N'blocking'
|
||||
FROM @Cred c WHERE c.Acceptable = 0;
|
||||
INSERT @c6
|
||||
SELECT b.DriverType, b.DriverName, b.DriverInstanceId, b.BlobKind, b.OwnerName, N'(whole blob)', N'config is not valid JSON (credential fields cannot be checked) — verify; comments/trailing commas may be a false positive', NULL, N'blocking'
|
||||
FROM @Blob b WHERE ISJSON(b.Config) = 0 AND LTRIM(RTRIM(ISNULL(b.Config, N''))) <> N'';
|
||||
INSERT @c6
|
||||
SELECT p.DriverType, p.DriverName, p.DriverInstanceId, p.BlobKind, p.OwnerName, p.FieldPath,
|
||||
CASE WHEN p.PropType <> 1 THEN N'#836 UserCertificatePath is not a string'
|
||||
ELSE N'#836 UserCertificatePath must be a relative path inside Drivers:OpcUaClient:UserCertificateDirectory' END,
|
||||
CASE WHEN p.PropType = 1 THEN p.PropValue END,
|
||||
CASE WHEN a.AuthType IS NULL OR a.AuthTypeType = 0 OR (a.AuthTypeType = 1 AND LOWER(a.AuthType) = N'certificate')
|
||||
OR (a.AuthTypeType = 2 AND (TRY_CAST(a.AuthType AS int) IS NULL OR TRY_CAST(a.AuthType AS int) = 2)) OR a.AuthTypeType NOT IN (0, 1, 2)
|
||||
THEN N'blocking' ELSE N'review' END
|
||||
FROM @Prop p
|
||||
OUTER APPLY (SELECT TOP 1 o.[value] AS AuthType, o.[type] AS AuthTypeType FROM OPENJSON(p.ParentJson) o WHERE LOWER(o.[key]) = N'authtype') a
|
||||
WHERE p.DriverType = N'OpcUaClient' AND LOWER(p.PropKey) = N'usercertificatepath'
|
||||
AND p.PropType <> 0 AND NOT (p.PropType = 1 AND p.PropValue = N'')
|
||||
AND (p.PropType <> 1
|
||||
OR LEN(p.PropValue) > 260
|
||||
OR LEFT(p.PropValue, 1) IN (N'/', N'\')
|
||||
OR p.PropValue LIKE N'_:%'
|
||||
OR N'/' + REPLACE(p.PropValue, N'\', N'/') + N'/' LIKE N'%//%'
|
||||
OR N'/' + REPLACE(p.PropValue, N'\', N'/') + N'/' LIKE N'%/./%'
|
||||
OR N'/' + REPLACE(p.PropValue, N'\', N'/') + N'/' LIKE N'%/../%'
|
||||
OR REPLACE(p.PropValue, N'\', N'/') LIKE N'%[^-A-Za-z0-9._/]%');
|
||||
SELECT N'6' AS check_id, N'#815/#836 CredentialReferenceOutOfScope' AS title,
|
||||
CASE WHEN SUM(CASE WHEN severity = N'blocking' THEN 1 ELSE 0 END) > 0 THEN N'attention' WHEN COUNT(*) > 0 THEN N'info' ELSE N'ok' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'CredentialReferenceDeployChecks / DriverCredentialPolicy; commits a224fd63, 63331b04, 3dbfe6ec' AS source FROM @c6;
|
||||
SELECT * FROM @c6 ORDER BY severity, DriverType, DriverName, field_path;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 7 — #835/#849 CredentialReferenceOtherInstance (BLOCKING)
|
||||
master: DriverCredentialPolicy.DescribeOtherInstanceProblems via CredentialReferenceDeployChecks (63331b04 #835,
|
||||
4a6229e4 #849, d68dff09). An in-scope reference whose instance part names ANOTHER driver of the same type:
|
||||
secret:<ns>/<X>/... X equals another driver's id or name (case-insensitive);
|
||||
env:<PREFIX><X>... X equals another driver's id/name in env form (ASCII letters upper-cased, digits kept, every
|
||||
other char '_'), alone or followed by '_'.
|
||||
An id-shaped part (secret DRV-<12 hex>, env DRV_<12 HEX>) matches by id only.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @Drv TABLE (DriverInstanceId nvarchar(64), Name nvarchar(128), DriverType nvarchar(32), IdNorm nvarchar(256) COLLATE Latin1_General_BIN2, NameNorm nvarchar(256) COLLATE Latin1_General_BIN2);
|
||||
INSERT @Drv
|
||||
SELECT d.DriverInstanceId, d.Name, d.DriverType,
|
||||
CASE WHEN d.DriverInstanceId COLLATE Latin1_General_BIN2 LIKE N'%[A-Za-z0-9]%' THEN
|
||||
(SELECT CASE WHEN SUBSTRING(d.DriverInstanceId, n.n, 1) COLLATE Latin1_General_BIN2 LIKE N'[A-Za-z0-9]' THEN UPPER(SUBSTRING(d.DriverInstanceId, n.n, 1)) ELSE N'_' END
|
||||
FROM @Num n WHERE n.n <= DATALENGTH(d.DriverInstanceId) / 2 ORDER BY n.n FOR XML PATH('')) ELSE N'' END,
|
||||
CASE WHEN d.Name COLLATE Latin1_General_BIN2 LIKE N'%[A-Za-z0-9]%' THEN
|
||||
(SELECT CASE WHEN SUBSTRING(d.Name, n.n, 1) COLLATE Latin1_General_BIN2 LIKE N'[A-Za-z0-9]' THEN UPPER(SUBSTRING(d.Name, n.n, 1)) ELSE N'_' END
|
||||
FROM @Num n WHERE n.n <= DATALENGTH(d.Name) / 2 ORDER BY n.n FOR XML PATH('')) ELSE N'' END
|
||||
FROM dbo.DriverInstance d WHERE d.DriverType IN (SELECT DriverType FROM @Field);
|
||||
|
||||
DECLARE @InstRef TABLE (DriverInstanceId nvarchar(64), DriverName nvarchar(128), DriverType nvarchar(32), BlobKind nvarchar(30), OwnerName nvarchar(128), FieldPath nvarchar(4000),
|
||||
Kind nvarchar(10), Part nvarchar(4000) COLLATE Latin1_General_BIN2, IdShaped bit, Shown nvarchar(4000));
|
||||
INSERT @InstRef
|
||||
SELECT c.DriverInstanceId, c.DriverName, c.DriverType, c.BlobKind, c.OwnerName, c.FieldPath, c.Kind, x.Part,
|
||||
CASE WHEN c.Kind = N'secret' THEN CASE WHEN LEN(x.Part) = 16 AND UPPER(LEFT(x.Part, 4)) = N'DRV-' AND SUBSTRING(x.Part, 5, 12) NOT LIKE N'%[^0-9A-Fa-f]%' THEN 1 ELSE 0 END
|
||||
ELSE CASE WHEN LEFT(x.Part, 4) = N'DRV_' AND LEN(x.Part) >= 16 AND SUBSTRING(x.Part, 5, 12) NOT LIKE N'%[^0-9A-F]%'
|
||||
AND (LEN(x.Part) = 16 OR SUBSTRING(x.Part, 17, 1) = N'_') THEN 1 ELSE 0 END END,
|
||||
c.Shown
|
||||
FROM @Cred c
|
||||
CROSS APPLY (SELECT TOP 1 n.Prefix FROM @Ns n WHERE n.DriverType = c.DriverType AND n.Kind = c.Kind AND LEFT(c.RefName, LEN(n.Prefix)) = n.Prefix ORDER BY LEN(n.Prefix) DESC) ns
|
||||
CROSS APPLY (SELECT SUBSTRING(c.RefName, LEN(ns.Prefix) + 1, 4000) AS Rest) r
|
||||
CROSS APPLY (SELECT CASE WHEN c.Kind = N'secret' AND CHARINDEX(N'/', r.Rest) > 0 THEN LEFT(r.Rest, CHARINDEX(N'/', r.Rest) - 1) ELSE r.Rest END AS Part) x
|
||||
WHERE c.Kind IN (N'secret', N'env');
|
||||
|
||||
DECLARE @c7 TABLE (DriverType nvarchar(32), DriverName nvarchar(128), DriverInstanceId nvarchar(64), blob nvarchar(30), owner_name nvarchar(128), field_path nvarchar(4000),
|
||||
reference nvarchar(4000), names_other_driver nvarchar(128), other_driver_id nvarchar(64), matched_on nvarchar(10));
|
||||
INSERT @c7
|
||||
SELECT i.DriverType, i.DriverName, i.DriverInstanceId, i.BlobKind, i.OwnerName, i.FieldPath, i.Shown, o.Name, o.DriverInstanceId,
|
||||
CASE WHEN (i.Kind = N'secret' AND LOWER(i.Part) = LOWER(o.DriverInstanceId) COLLATE Latin1_General_BIN2)
|
||||
OR (i.Kind = N'env' AND o.IdNorm <> N'' AND (i.Part = o.IdNorm OR LEFT(i.Part, LEN(o.IdNorm) + 1) = o.IdNorm + N'_')) THEN N'id' ELSE N'name' END
|
||||
FROM @InstRef i
|
||||
INNER JOIN @Drv o ON LOWER(o.DriverType) = LOWER(i.DriverType) AND o.DriverInstanceId <> i.DriverInstanceId
|
||||
WHERE (i.Kind = N'secret' AND (LOWER(i.Part) = LOWER(o.DriverInstanceId) COLLATE Latin1_General_BIN2 OR (i.IdShaped = 0 AND LOWER(i.Part) = LOWER(o.Name) COLLATE Latin1_General_BIN2)))
|
||||
OR (i.Kind = N'env' AND (
|
||||
(o.IdNorm <> N'' AND (i.Part = o.IdNorm OR LEFT(i.Part, LEN(o.IdNorm) + 1) = o.IdNorm + N'_'))
|
||||
OR (i.IdShaped = 0 AND o.NameNorm <> N'' AND (i.Part = o.NameNorm OR LEFT(i.Part, LEN(o.NameNorm) + 1) = o.NameNorm + N'_'))));
|
||||
SELECT N'7' AS check_id, N'#835/#849 CredentialReferenceOtherInstance' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'DriverCredentialPolicy.DescribeOtherInstanceProblems; commits 63331b04, 4a6229e4, d68dff09' AS source FROM @c7;
|
||||
SELECT * FROM @c7 ORDER BY DriverType, DriverName, field_path;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 8 — #848/#849 reserved / claimed driver names (AUTHORING, not a deploy refusal)
|
||||
master: DriverCredentialPolicy.IsReservedInstanceName + DriverConfigSaveGate.DescribeNameChangeProblemsAsync (d08e59ee,
|
||||
d68dff09). A credential-bearing driver named 'drv' / 'DRV-<12 hex>...' (env form DRV or DRV_<12 HEX>) cannot be
|
||||
created or renamed to that name; listed so a later rename/save surprise is expected.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c8 TABLE (DriverType nvarchar(32), DriverName nvarchar(128), DriverInstanceId nvarchar(64), env_form nvarchar(256), finding nvarchar(200));
|
||||
INSERT @c8
|
||||
SELECT d.DriverType, d.Name, d.DriverInstanceId, d.NameNorm, N'name is reserved (collides with the DRV-<id> credential convention)'
|
||||
FROM @Drv d
|
||||
WHERE d.NameNorm = N'DRV'
|
||||
OR (LEFT(d.NameNorm, 4) = N'DRV_' AND LEN(d.NameNorm) >= 16 AND SUBSTRING(d.NameNorm, 5, 12) NOT LIKE N'%[^0-9A-F]%'
|
||||
AND (LEN(d.NameNorm) = 16 OR SUBSTRING(d.NameNorm, 17, 1) = N'_'));
|
||||
SELECT N'8' AS check_id, N'#848/#849 reserved driver names (authoring only)' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'NO (save/rename refused)' AS blocking, N'DriverCredentialPolicy.IsReservedInstanceName; commits d08e59ee, d68dff09' AS source FROM @c8;
|
||||
SELECT * FROM @c8;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 9 — MTConnectMultipleDevices (BLOCKING)
|
||||
master: DraftValidator.ValidateMTConnectSingleDevice (dcc827ca, #628). More than one /raw Device under one
|
||||
MTConnect driver instance.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c9 TABLE (DriverInstanceId nvarchar(64), DriverName nvarchar(128), device_count int, devices nvarchar(max));
|
||||
INSERT @c9
|
||||
SELECT d.DriverInstanceId, d.Name, COUNT(*),
|
||||
STUFF((SELECT N', ' + v2.Name FROM dbo.Device v2 WHERE v2.DriverInstanceId = d.DriverInstanceId FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N'')
|
||||
FROM dbo.DriverInstance d INNER JOIN dbo.Device v ON v.DriverInstanceId = d.DriverInstanceId
|
||||
WHERE d.DriverType COLLATE Latin1_General_BIN2 = N'MTConnect'
|
||||
GROUP BY d.DriverInstanceId, d.Name HAVING COUNT(*) > 1;
|
||||
SELECT N'9' AS check_id, N'MTConnectMultipleDevices' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'DraftValidator.ValidateMTConnectSingleDevice; commit dcc827ca (#628)' AS source FROM @c9;
|
||||
SELECT * FROM @c9;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 10 — #781 Sql tag gates
|
||||
10a SqlQueryNotDeclared (BLOCKING, approximate): a Query-model tag ("model":"Query") whose "query" is not a key of
|
||||
the driver's merged "queries" map (DriverConfig, shallow-overridden by the DeviceConfig when the driver has
|
||||
exactly one Device — DriverDeviceConfigMerger). master: SqlTagDeployChecks.UndeclaredQueryErrors (241318e7).
|
||||
10b Sql tags carrying a "write" block (REVIEW): the block is now parsed (f5c30ed0); an invalid block is
|
||||
SqlWriteBlockInvalid (BLOCKING, and the driver would skip the whole tag), Upsert/Procedure on an Odbc provider
|
||||
is SqlWriteModeUnsupported (BLOCKING). master: SqlTagDeployChecks.WriteBlockErrors.
|
||||
10c Writable mismatch (WARNING only, logged at deploy): Tag.AccessLevel=ReadWrite vs TagConfig "writable": true.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c10 TABLE (sub nvarchar(4), TagId nvarchar(64), RawPath nvarchar(4000), DriverName nvarchar(128), finding nvarchar(300), severity nvarchar(10));
|
||||
WITH sqlTag AS (
|
||||
SELECT r.*, (SELECT COUNT(*) FROM dbo.Device v WHERE v.DriverInstanceId = r.DriverInstanceId) AS DeviceCount
|
||||
FROM @RawTag r WHERE r.DriverType = N'Sql' AND ISJSON(r.TagConfig) = 1
|
||||
)
|
||||
INSERT @c10
|
||||
SELECT N'10a', t.TagId, t.RawPath, t.DriverName, N'query ''' + q.QueryName + N''' not declared under the driver''s queries', N'blocking'
|
||||
FROM sqlTag t
|
||||
CROSS APPLY (SELECT JSON_VALUE(t.TagConfig, '$.query') AS QueryName, JSON_VALUE(t.TagConfig, '$.model') AS Model) q
|
||||
OUTER APPLY (SELECT TOP 1 v.DeviceConfig FROM dbo.Device v WHERE v.DriverInstanceId = t.DriverInstanceId AND t.DeviceCount = 1
|
||||
AND ISJSON(v.DeviceConfig) = 1 AND EXISTS (SELECT 1 FROM OPENJSON(v.DeviceConfig) o WHERE o.[key] COLLATE Latin1_General_BIN2 = N'queries')) dev
|
||||
CROSS APPLY (SELECT ISNULL(dev.DeviceConfig, (SELECT di.DriverConfig FROM dbo.DriverInstance di WHERE di.DriverInstanceId = t.DriverInstanceId)) AS Cfg) m
|
||||
WHERE LOWER(q.Model) = N'query' AND q.QueryName IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM OPENJSON(CASE WHEN ISJSON(m.Cfg) = 1 THEN m.Cfg END) top1
|
||||
CROSS APPLY OPENJSON(CASE WHEN top1.[type] = 5 THEN top1.[value] END) qk
|
||||
WHERE LOWER(top1.[key]) = N'queries' AND qk.[key] COLLATE Latin1_General_BIN2 = q.QueryName COLLATE Latin1_General_BIN2);
|
||||
INSERT @c10
|
||||
SELECT N'10b', r.TagId, r.RawPath, r.DriverName, N'TagConfig has a "write" block — verify it parses (mode/table/keys) and the provider serves the mode', N'review'
|
||||
FROM @RawTag r WHERE r.DriverType = N'Sql' AND ISJSON(r.TagConfig) = 1
|
||||
AND EXISTS (SELECT 1 FROM OPENJSON(r.TagConfig) o WHERE o.[key] COLLATE Latin1_General_BIN2 = N'write');
|
||||
INSERT @c10
|
||||
SELECT N'10c', r.TagId, r.RawPath, r.DriverName,
|
||||
CASE WHEN r.AccessLevel = N'ReadWrite' THEN N'AccessLevel ReadWrite but TagConfig lacks "writable": true — writes refused'
|
||||
ELSE N'TagConfig "writable": true but AccessLevel is not ReadWrite — node read-only' END, N'warning'
|
||||
FROM @RawTag r
|
||||
CROSS APPLY (SELECT CASE WHEN EXISTS (SELECT 1 FROM OPENJSON(CASE WHEN ISJSON(r.TagConfig) = 1 THEN r.TagConfig END) o
|
||||
WHERE o.[key] COLLATE Latin1_General_BIN2 = N'writable' AND o.[type] = 3 AND o.[value] = N'true') THEN 1 ELSE 0 END AS BlobWritable) w
|
||||
WHERE r.DriverType = N'Sql' AND (CASE WHEN r.AccessLevel = N'ReadWrite' THEN 1 ELSE 0 END) <> w.BlobWritable;
|
||||
SELECT N'10' AS check_id, N'#781 Sql tag gates' AS title,
|
||||
CASE WHEN SUM(CASE WHEN severity IN (N'blocking', N'review') THEN 1 ELSE 0 END) > 0 THEN N'attention' WHEN COUNT(*) > 0 THEN N'info' ELSE N'ok' END AS status,
|
||||
COUNT(*) AS row_count, N'10a/10b YES, 10c NO' AS blocking, N'SqlTagDeployChecks; commits 241318e7, f5c30ed0' AS source FROM @c10;
|
||||
SELECT * FROM @c10 ORDER BY sub, RawPath;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 11 — #803 Heidenhain gates (BLOCKING) — sanity only: the Heidenhain driver type did not exist at b8b4c18f,
|
||||
so any row here was written by something other than the b8b4c18f AdminUI.
|
||||
master: HeidenhainDeployChecks (2b45694f).
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c11 TABLE (DriverInstanceId nvarchar(64), DriverName nvarchar(128));
|
||||
INSERT @c11 SELECT DriverInstanceId, Name FROM dbo.DriverInstance WHERE LOWER(DriverType) = N'heidenhain';
|
||||
SELECT N'11' AS check_id, N'#803 Heidenhain drivers present (expected none)' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'HeidenhainDeployChecks; commit 2b45694f' AS source FROM @c11;
|
||||
SELECT * FROM @c11;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 12 — UNS effective-name collisions (pre-existing rule, unchanged; re-checked because the whole DB is the
|
||||
draft). master: DraftValidator.ValidateUnsEffectiveLeafUniqueness. References (DisplayNameOverride else raw tag
|
||||
Name), VirtualTags and ScriptedAlarms share {EquipmentId}/{EffectiveName}; compared ordinally.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @Leaf TABLE (EquipmentId nvarchar(64) COLLATE Latin1_General_BIN2, EffName nvarchar(128) COLLATE Latin1_General_BIN2, Src nvarchar(200));
|
||||
INSERT @Leaf SELECT u.EquipmentId, ISNULL(u.DisplayNameOverride, t.Name), N'UnsTagReference ' + u.UnsTagReferenceId
|
||||
FROM dbo.UnsTagReference u LEFT JOIN dbo.Tag t ON t.TagId = u.TagId WHERE ISNULL(u.DisplayNameOverride, t.Name) <> N'';
|
||||
INSERT @Leaf SELECT EquipmentId, Name, N'VirtualTag ' + VirtualTagId FROM dbo.VirtualTag WHERE Name <> N'';
|
||||
INSERT @Leaf SELECT EquipmentId, Name, N'ScriptedAlarm ' + ScriptedAlarmId FROM dbo.ScriptedAlarm WHERE Name <> N'';
|
||||
DECLARE @c12 TABLE (EquipmentId nvarchar(64), EffectiveName nvarchar(128), sources nvarchar(max));
|
||||
INSERT @c12
|
||||
SELECT l.EquipmentId, l.EffName,
|
||||
STUFF((SELECT N', ' + l2.Src FROM @Leaf l2 WHERE l2.EquipmentId = l.EquipmentId AND l2.EffName = l.EffName FOR XML PATH(''), TYPE).value('.', 'nvarchar(max)'), 1, 2, N'')
|
||||
FROM @Leaf l GROUP BY l.EquipmentId, l.EffName HAVING COUNT(*) > 1;
|
||||
SELECT N'12' AS check_id, N'UnsEffectiveName collisions (pre-existing rule)' AS title, CASE WHEN COUNT(*) = 0 THEN N'ok' ELSE N'attention' END AS status,
|
||||
COUNT(*) AS row_count, N'YES' AS blocking, N'DraftValidator.ValidateUnsEffectiveLeafUniqueness (unchanged since b8b4c18f)' AS source FROM @c12;
|
||||
SELECT * FROM @c12 ORDER BY EquipmentId, EffectiveName;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 13 — FOCAS tags whose authored address or value changes under master (the box's one driver)
|
||||
Not deploy refusals: the TagConfig inspector reports 'String' mismatches at deploy as WARNINGS
|
||||
(Deployment:TagConfigValidationMode = Warn, the default) and the driver skips that tag (BadNodeIdUnknown).
|
||||
rename FIXED: ...MaxRpm -> MotorSpeedPct (1dba46bd)
|
||||
rename FIXED:Axes/AA_1, AA_2 -> AA6, AA7 on the Makino (9460da51 — old names were a decode artefact)
|
||||
refused dataType String on a PMC / PARAM: / MACRO: address (cc140593, R07:U8)
|
||||
value FIXED:Axes/... positions scaled by the block envelope (69e0a622: 2801574 -> 280.1574)
|
||||
value servo/spindle Load: real percent, spindle Load Float64 (1dba46bd)
|
||||
value MACRO: decoded at the right offset (665ecd55: was off by 10^n); vacant -> BadNoData,
|
||||
fractional on an integer tag -> BadTypeMismatch
|
||||
value FIXED:Program/...: an unreported field -> BadNoData instead of Good 0 (26b512ad)
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
DECLARE @c13 TABLE (TagId nvarchar(64), RawPath nvarchar(4000), address nvarchar(400), tag_config_dataType nvarchar(40), entity_DataType nvarchar(32), finding nvarchar(300), severity nvarchar(10));
|
||||
INSERT @c13
|
||||
SELECT r.TagId, r.RawPath, a.Addr, a.Dt, r.DataType, f.Finding, f.Severity
|
||||
FROM @RawTag r
|
||||
CROSS APPLY (SELECT LTRIM(RTRIM(JSON_VALUE(CASE WHEN ISJSON(r.TagConfig) = 1 THEN r.TagConfig END, '$.address'))) AS Addr,
|
||||
JSON_VALUE(CASE WHEN ISJSON(r.TagConfig) = 1 THEN r.TagConfig END, '$.dataType') AS Dt) a
|
||||
CROSS APPLY (
|
||||
SELECT N'rename: MaxRpm is now MotorSpeedPct — re-point the tag (and ScadaBridge)' AS Finding, N'attention' AS Severity WHERE a.Addr LIKE N'%MaxRpm%'
|
||||
UNION ALL SELECT N'rename: AA_1/AA_2 are now the real axis names (AA6/AA7 on Z-34184) — re-point', N'attention' WHERE a.Addr LIKE N'%AA[_]1%' OR a.Addr LIKE N'%AA[_]2%'
|
||||
UNION ALL SELECT N'refused: String on a PMC/PARAM/MACRO address — driver skips the tag', N'attention'
|
||||
WHERE LOWER(a.Dt) = N'string' AND LOWER(LEFT(a.Addr, 6)) <> N'fixed:' AND LOWER(LEFT(a.Addr, 6)) <> N'alarm:'
|
||||
UNION ALL SELECT N'value change: position now scaled by its envelope', N'info' WHERE LOWER(a.Addr) LIKE N'fixed:axes/%' AND LOWER(a.Addr) LIKE N'%position%'
|
||||
UNION ALL SELECT N'value change: load now a real percent (spindle Load Float64)', N'info' WHERE LOWER(a.Addr) LIKE N'fixed:%load%'
|
||||
UNION ALL SELECT N'value change: macro decoded correctly (magnitude changes); vacant = BadNoData', N'info' WHERE LOWER(a.Addr) LIKE N'macro:%'
|
||||
UNION ALL SELECT N'value change: unreported program field reads BadNoData, not Good 0', N'info' WHERE LOWER(a.Addr) LIKE N'fixed:program%'
|
||||
) f
|
||||
WHERE r.DriverType = N'FOCAS';
|
||||
SELECT N'13' AS check_id, N'FOCAS address renames / refusals / value changes' AS title,
|
||||
CASE WHEN SUM(CASE WHEN severity = N'attention' THEN 1 ELSE 0 END) > 0 THEN N'attention' WHEN COUNT(*) > 0 THEN N'info' ELSE N'ok' END AS status,
|
||||
COUNT(*) AS row_count, N'NO (warn at deploy; client-visible)' AS blocking, N'commits 1dba46bd, 9460da51, cc140593, 69e0a622, 665ecd55, 26b512ad' AS source FROM @c13;
|
||||
SELECT * FROM @c13 ORDER BY severity, RawPath;
|
||||
|
||||
/* ---------------------------------------------------------------------------------------------
|
||||
Check 14 — inventory (INFO): what the box actually runs, so the owner can discount driver-specific rows.
|
||||
--------------------------------------------------------------------------------------------- */
|
||||
SELECT N'14' AS check_id, N'Inventory' AS title, N'info' AS status,
|
||||
(SELECT COUNT(*) FROM dbo.DriverInstance) AS row_count, N'NO' AS blocking, N'-' AS source;
|
||||
SELECT d.DriverType, COUNT(DISTINCT d.DriverInstanceId) AS drivers, SUM(CASE WHEN d.Enabled = 1 THEN 1 ELSE 0 END) AS enabled_driver_rows,
|
||||
(SELECT COUNT(*) FROM @RawTag r WHERE r.DriverType = d.DriverType COLLATE Latin1_General_BIN2) AS raw_tags
|
||||
FROM dbo.DriverInstance d GROUP BY d.DriverType
|
||||
UNION ALL SELECT N'(VirtualTag)', COUNT(*), SUM(CASE WHEN Enabled = 1 THEN 1 ELSE 0 END), NULL FROM dbo.VirtualTag
|
||||
UNION ALL SELECT N'(ScriptedAlarm)', COUNT(*), SUM(CASE WHEN Enabled = 1 THEN 1 ELSE 0 END), NULL FROM dbo.ScriptedAlarm
|
||||
UNION ALL SELECT N'(Script)', COUNT(*), NULL, NULL FROM dbo.Script
|
||||
UNION ALL SELECT N'(UnsTagReference)', COUNT(*), NULL, NULL FROM dbo.UnsTagReference
|
||||
UNION ALL SELECT N'(ApiKey)', COUNT(*), SUM(CASE WHEN Enabled = 1 THEN 1 ELSE 0 END), NULL FROM dbo.ApiKey
|
||||
UNION ALL SELECT N'(ClusterNode)', COUNT(*), SUM(CASE WHEN Enabled = 1 THEN 1 ELSE 0 END), NULL FROM dbo.ClusterNode;
|
||||
Reference in New Issue
Block a user