Compare commits
181 Commits
renumber-1
...
pxe-webapp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f7954ac88 | ||
|
|
120553c975 | ||
|
|
b9a4d0230d | ||
|
|
04db6d6605 | ||
|
|
b11e678297 | ||
|
|
9af1c6d89a | ||
|
|
d63cc5761b | ||
|
|
821a927365 | ||
|
|
9d75d03b2d | ||
|
|
bca04f4ca2 | ||
|
|
576955785d | ||
|
|
23ee99a390 | ||
|
|
9d56a5369e | ||
|
|
dde1aacccb | ||
|
|
54015566a7 | ||
|
|
c8e0b4eace | ||
|
|
da6e62c084 | ||
|
|
0b1625ed87 | ||
|
|
b20e6be629 | ||
|
|
21ff95af78 | ||
|
|
606659bf07 | ||
|
|
53d39bfc50 | ||
|
|
24c41d7e87 | ||
|
|
2717439690 | ||
|
|
bcd91b488a | ||
|
|
bab6927bdb | ||
|
|
c1a5fb3e02 | ||
|
|
86b4c1f301 | ||
|
|
18f20b00e2 | ||
|
|
6dc9761e67 | ||
|
|
29d815fbeb | ||
|
|
45cf7be37a | ||
|
|
ead3314f58 | ||
|
|
689eebf9d1 | ||
|
|
d3a0f90ca1 | ||
|
|
bc422649e9 | ||
|
|
28fd9fe474 | ||
|
|
454e30ff6f | ||
|
|
0d69222f85 | ||
|
|
63fafbfc73 | ||
|
|
045a1fb97f | ||
|
|
a94c20331a | ||
|
|
81c2d85301 | ||
|
|
4db9dc093f | ||
|
|
2dbaaded44 | ||
|
|
a54df7ebd9 | ||
|
|
2ed2cbe2e9 | ||
|
|
2bcc9d8016 | ||
|
|
fd00d5ff89 | ||
|
|
a6f95fcca1 | ||
|
|
cc1e76f3ab | ||
|
|
731d7b044d | ||
|
|
0b86255711 | ||
|
|
35cad62733 | ||
|
|
dfd57bef17 | ||
|
|
24b4d9df66 | ||
|
|
aa026bace0 | ||
|
|
6187219378 | ||
|
|
92f1e62966 | ||
|
|
ae806cf98e | ||
|
|
28cd977c9e | ||
|
|
bfd5c5b75e | ||
|
|
dd9ee33cd5 | ||
|
|
397063f987 | ||
|
|
ddf0a2fdf2 | ||
|
|
6fac73c947 | ||
|
|
f0c049376c | ||
|
|
b691a82530 | ||
|
|
e43ab2a652 | ||
|
|
83ba4865e8 | ||
|
|
121577f090 | ||
|
|
6fcdc8ddb5 | ||
|
|
8c36d1ba5d | ||
|
|
12f6f00cdb | ||
|
|
020f043d8b | ||
|
|
44a7eeae95 | ||
|
|
eff956ae6c | ||
|
|
19049e6b1f | ||
|
|
a97faf9fad | ||
|
|
165d6d34a3 | ||
|
|
b50ff36f71 | ||
|
|
143c27c3bf | ||
|
|
b3ed4cda81 | ||
|
|
14eab13a1a | ||
|
|
783f2fece0 | ||
|
|
1f5f9eb60a | ||
|
|
77655bbfcd | ||
|
|
372c3802e5 | ||
|
|
25f65424c7 | ||
|
|
29e0d97f59 | ||
|
|
b474ac376e | ||
|
|
96c4e5dd11 | ||
|
|
678d10d5c2 | ||
|
|
737a729548 | ||
|
|
47f5d4603a | ||
|
|
5e249e8ad0 | ||
|
|
eb4d067f12 | ||
|
|
f1755751bb | ||
|
|
8ba86ba560 | ||
|
|
f99ece1366 | ||
|
|
ba4df185cd | ||
|
|
bd131f6da9 | ||
|
|
18168b54f8 | ||
|
|
b228a69364 | ||
|
|
abbdc9873c | ||
|
|
280a3b3ba4 | ||
|
|
e40a294193 | ||
|
|
12c6c771dc | ||
|
|
d479e6823c | ||
|
|
ad075c255c | ||
|
|
6e8b8a83a7 | ||
|
|
6f86c81a44 | ||
|
|
2415e6cfd6 | ||
|
|
f3df6c7175 | ||
|
|
26d14b0684 | ||
|
|
cc20501e2e | ||
|
|
0d9c4fdac4 | ||
|
|
54cbe6b5d6 | ||
|
|
b075939c38 | ||
|
|
6d5fee786c | ||
|
|
0e1b80b903 | ||
|
|
8907a86c42 | ||
|
|
a7cb7164d9 | ||
|
|
28efde7d76 | ||
|
|
d5cf255443 | ||
|
|
7ed30f9b85 | ||
|
|
c4aeaaaa17 | ||
|
|
54176e591f | ||
|
|
d185e2b810 | ||
|
|
68f2ce29e4 | ||
|
|
f442ff2f65 | ||
|
|
a0aceb4680 | ||
|
|
bb08392b84 | ||
|
|
9d51c0b987 | ||
|
|
68df59e117 | ||
|
|
d2200e8522 | ||
|
|
8c21282024 | ||
|
|
36be60e9ae | ||
|
|
b96ff7e970 | ||
|
|
d04865da0f | ||
|
|
2b6189f739 | ||
|
|
385acfefa7 | ||
|
|
d5f7abc19f | ||
|
|
049dd7bde6 | ||
|
|
c7b17696d0 | ||
|
|
dfa026b4ee | ||
|
|
936902dc4d | ||
|
|
27e76467a8 | ||
|
|
66c24b5d59 | ||
|
|
1a175bc4fe | ||
|
|
01f524e8b3 | ||
|
|
3880151e71 | ||
|
|
5f97e81dec | ||
|
|
626561a1fa | ||
|
|
f1d9bdf478 | ||
|
|
cc391529bd | ||
|
|
234295931e | ||
|
|
0ffdcc79ed | ||
|
|
3b63697176 | ||
|
|
0cb6b26c27 | ||
|
|
8fbae24b4d | ||
|
|
18db077475 | ||
|
|
8df30017aa | ||
|
|
b240cfea12 | ||
|
|
e7be0f5391 | ||
|
|
0c4c2a245d | ||
|
|
fdf63ab32f | ||
|
|
64e1f7e088 | ||
|
|
287ec86c12 | ||
|
|
85e7d91b1a | ||
|
|
2461804fc8 | ||
|
|
48bc609eb5 | ||
|
|
132c57ab39 | ||
|
|
99deaa6882 | ||
|
|
948d013539 | ||
|
|
6e218f3daf | ||
|
|
b5b644a360 | ||
|
|
bc89ba1cf3 | ||
|
|
1487abdba5 | ||
|
|
7af66575d0 | ||
|
|
8e11f0824a |
20
.githooks/pre-commit
Executable file
20
.githooks/pre-commit
Executable file
@@ -0,0 +1,20 @@
|
||||
#!/bin/bash
|
||||
# Refuse a commit that stages an unattend answer file the linter rejects.
|
||||
# A bad answer file stops every bay imaged with it at "Windows could not
|
||||
# parse or process unattend answer file". Enable with:
|
||||
# git config core.hooksPath .githooks
|
||||
set -euo pipefail
|
||||
ROOT="$(git rev-parse --show-toplevel)"
|
||||
mapfile -t FILES < <(git diff --cached --name-only --diff-filter=ACMR | grep -iE '(^|/)(Flat)?Unattend[^/]*\.xml$' || true)
|
||||
[ ${#FILES[@]} -eq 0 ] && exit 0
|
||||
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
||||
STAGED=()
|
||||
for f in "${FILES[@]}"; do
|
||||
out="$TMP/$(echo "$f" | tr '/' '_')"
|
||||
git show ":$f" > "$out" # lint what is staged, not the working copy
|
||||
STAGED+=("$out")
|
||||
done
|
||||
if ! python3 "$ROOT/scripts/lint-unattend.py" --local "${STAGED[@]}"; then
|
||||
echo "pre-commit: unattend lint failed for: ${FILES[*]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -118,3 +118,17 @@ playbook/shopfloor-setup/gea-shopfloor-partmarker/PartMarker/Mark/utilpassword.t
|
||||
# HeatTreat per-machine DNC .reg exports (6601-6604) - contain DNC FtpPasswd
|
||||
# credentials. Deployed via the enrollment share from the working tree.
|
||||
playbook/shopfloor-setup/gea-shopfloor-heattreat/reg/*.reg
|
||||
|
||||
# SIC e10 controller config files carry the programmer/supervisor password
|
||||
# in cleartext. Never commit them. See reference/e10/README.md
|
||||
reference/e10/**/ConfigCtrl*
|
||||
reference/e10/**/*.kind1
|
||||
# GE part programs carry part numbers and marking content. Keep them out of
|
||||
# git; they live in ~/pxe-images/e10-testing/partprograms/
|
||||
reference/e10/partprograms/
|
||||
|
||||
# Per-site playbook values (NIC MACs); template: playbook/site-vars.example.yml
|
||||
playbook/site-vars.yml
|
||||
|
||||
# Site content pulled from a live server (scripts/capture-site-content.sh)
|
||||
site-content/
|
||||
|
||||
29
Build-PxeUsb.cmd
Normal file
29
Build-PxeUsb.cmd
Normal file
@@ -0,0 +1,29 @@
|
||||
@echo off
|
||||
rem Build the PXE server installer USB. Double-click this file in the build
|
||||
rem folder (copied to a local disk, e.g. C:\pxe-usb). It asks for admin
|
||||
rem rights, lists the USB disks and asks which one to erase and build.
|
||||
rem Runs scripts\Build-PxeUsb.ps1 -Interactive with the execution policy
|
||||
rem bypassed for that one window only.
|
||||
setlocal
|
||||
set "PS1=%~dp0scripts\Build-PxeUsb.ps1"
|
||||
if not exist "%PS1%" (
|
||||
echo Cannot find %PS1%
|
||||
echo Run this from the pxe-usb build folder.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
if "%~d0"=="\\" goto :network
|
||||
net use %~d0 >nul 2>&1 && goto :network
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -Command "Start-Process powershell -Verb RunAs -ArgumentList '-NoProfile','-ExecutionPolicy','Bypass','-File','\"%PS1%\"','-Interactive'"
|
||||
exit /b 0
|
||||
|
||||
:network
|
||||
echo This build folder is on a network drive (%~d0).
|
||||
echo An administrator window cannot see network drives mapped in this session.
|
||||
echo Copy the whole pxe-usb folder to a local disk first, for example:
|
||||
echo.
|
||||
echo robocopy "%~dp0." C:\pxe-usb /E
|
||||
echo.
|
||||
echo then double-click C:\pxe-usb\Build-PxeUsb.cmd
|
||||
pause
|
||||
exit /b 1
|
||||
14
Capture-SiteContent.cmd
Normal file
14
Capture-SiteContent.cmd
Normal file
@@ -0,0 +1,14 @@
|
||||
@echo off
|
||||
rem Pull the site content (Office, Adobe, certificates, Engineering Layer,
|
||||
rem BIOS files, boot.wim, Blancco) from a running PXE server into this
|
||||
rem build folder. Double-click it; it asks for the server address and the
|
||||
rem pxe password. Runs scripts\Capture-SiteContent.ps1 -Interactive.
|
||||
setlocal
|
||||
set "PS1=%~dp0scripts\Capture-SiteContent.ps1"
|
||||
if not exist "%PS1%" (
|
||||
echo Cannot find %PS1%
|
||||
echo Run this from the pxe-usb build folder.
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" -Interactive
|
||||
516
README.md
516
README.md
@@ -1,265 +1,251 @@
|
||||
# GE Aerospace PXE Boot Server
|
||||
|
||||
Automated, air-gapped PXE boot server for deploying GE Aerospace Windows images. Built on Ubuntu 24.04 Server with zero-touch provisioning via autoinstall and Ansible.
|
||||
|
||||
## Overview
|
||||
|
||||
This project provides a complete, repeatable build process for a PXE boot server that serves Windows PE images to client machines on an isolated network. Everything runs offline after initial setup — no internet required on the target server.
|
||||
|
||||
### Boot Chain
|
||||
|
||||
```
|
||||
Client PXE boot (UEFI Secure Boot)
|
||||
-> iPXE (TFTP, Broadcom-signed for Secure Boot)
|
||||
-> iPXE boot menu (HTTP, port 4433)
|
||||
-> User selects boot option:
|
||||
├── Windows PE -> wimboot -> boot.wim -> startnet.cmd -> Samba share -> Image deployment
|
||||
├── Clonezilla -> vmlinuz/initrd -> Disk cloning/imaging
|
||||
├── Blancco -> Native kernel + initramfs -> NIST 800-88 drive erasure
|
||||
└── Memtest86+ -> Memory diagnostics
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|-------------|-----------|------------------------------------------|
|
||||
| dnsmasq | 67/udp | DHCP (172.16.9.10-100, 12h lease) |
|
||||
| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
|
||||
| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
|
||||
| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
|
||||
| Samba | 445/tcp | Deployment content + Clonezilla + Blancco|
|
||||
| Flask Webapp| 9009/tcp | Web management interface |
|
||||
|
||||
### Network
|
||||
|
||||
- **PXE server IP:** `172.16.9.1/24`
|
||||
- **DHCP range:** `172.16.9.10` - `172.16.9.100`
|
||||
- **Firewall:** UFW deny-by-default, only service ports open (22, 67, 69, 80, 445, 4433, 9009)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**On your workstation (internet-connected):**
|
||||
- Ubuntu 24.04 (or Linux Mint / similar) for downloading packages
|
||||
- Ubuntu Server 24.04 ISO
|
||||
- GE Aerospace Media Creator LITE (for WinPE images)
|
||||
- USB drive >= 8 GB (32+ GB if bundling WinPE images)
|
||||
|
||||
**GE Access Packages (MyAccess portal):**
|
||||
- EPM Rufus Exception Request
|
||||
- EPM DT Functions
|
||||
- DLP - Encrypted Removable (USB) Long Term Access
|
||||
|
||||
### Step 1: Download Offline Packages
|
||||
|
||||
```bash
|
||||
./scripts/download-packages.sh
|
||||
```
|
||||
|
||||
Downloads all .deb packages and Python wheels for offline installation (~140 MB of debs, ~20 MB of wheels).
|
||||
|
||||
### Step 2: Prepare Boot Tools (optional)
|
||||
|
||||
```bash
|
||||
./scripts/prepare-boot-tools.sh /path/to/blancco.iso /path/to/clonezilla.zip /path/to/memtest.bin
|
||||
```
|
||||
|
||||
Extracts and configures boot tool files (Blancco, Clonezilla, Memtest86+). Automatically patches Blancco's config.img to auto-save erasure reports to the PXE server's Samba share.
|
||||
|
||||
### Step 3: Build the USB
|
||||
|
||||
```bash
|
||||
sudo ./scripts/build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
|
||||
```
|
||||
|
||||
Creates a bootable USB with two partitions:
|
||||
- **Partition 1:** Ubuntu Server installer
|
||||
- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
|
||||
|
||||
### Step 4: Install on Target Server
|
||||
|
||||
1. Insert USB into the target machine
|
||||
2. Press F12 and boot from USB
|
||||
3. Ubuntu auto-installs with no interaction
|
||||
4. After reboot, the first-boot script:
|
||||
- Installs all offline .deb packages
|
||||
- Runs the Ansible playbook (configures dnsmasq, Apache, Samba, UFW, webapp)
|
||||
- Configures static IP `172.16.9.1/24`
|
||||
5. Move the server's wired NIC to the isolated PXE switch
|
||||
|
||||
### Step 5: Access the Web Interface
|
||||
|
||||
Open `http://172.16.9.1:9009` from any machine on the isolated network.
|
||||
|
||||
## Web Management Interface
|
||||
|
||||
The Flask webapp (port 9009) provides a browser-based management UI:
|
||||
|
||||
- **Dashboard** — Service status overview, disk usage, connected DHCP clients
|
||||
- **Image Import** — Import WinPE deployment images from USB drives
|
||||
- **Unattend Editor** — Edit Windows unattend.xml files per image type (XML syntax highlighting)
|
||||
- **startnet.cmd Editor** — Modify the startnet.cmd inside boot.wim without Windows (uses wimtools)
|
||||
- **Clonezilla Backups** — Upload, download, and manage disk backup images
|
||||
- **Blancco Reports** — View, download, and manage drive erasure reports (auto-collected via Samba)
|
||||
- **Image Config** — Per-image configuration editor (drivers, OS packages, hardware models)
|
||||
- **Enrollment** — Upload, download, and manage GCCH bulk enrollment packages
|
||||
- **Audit Log** — Activity history for all write operations (imports, edits, deletes)
|
||||
|
||||
### Image Types Supported
|
||||
|
||||
| Image Type | Domain | Description |
|
||||
|----------------------|-----------------|-------------------------|
|
||||
| gea-standard | geaerospace.com | Standard desktop |
|
||||
| gea-engineer | geaerospace.com | Engineering desktop |
|
||||
| gea-shopfloor | geaerospace.com | Shop floor kiosk |
|
||||
| ge-standard | ge.com | Standard desktop |
|
||||
| ge-engineer | ge.com | Engineering desktop |
|
||||
| ge-shopfloor-lockdown| ge.com | Shop floor (locked) |
|
||||
| ge-shopfloor-mce | ge.com | Shop floor (MCE) |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pxe-server/
|
||||
├── autoinstall/
|
||||
│ ├── user-data # Cloud-init autoinstall config + first-boot script
|
||||
│ └── meta-data # Cloud-init metadata (required, empty)
|
||||
├── playbook/
|
||||
│ ├── pxe_server_setup.yml # Ansible playbook: all server configuration
|
||||
│ ├── inventory.ini # Ansible inventory
|
||||
│ ├── startnet.cmd # WinPE startup script (injected into boot.wim)
|
||||
│ ├── blancco-init.sh # Custom initramfs for Blancco PXE boot
|
||||
│ ├── blancco-preferences.xml # Blancco auto-report preferences
|
||||
│ ├── check-bios.cmd # Pre-imaging BIOS update checker
|
||||
│ ├── FlatUnattendW10-shopfloor.xml # Shopfloor unattend.xml
|
||||
│ └── shopfloor-setup/ # PC type setup scripts (Standard, CMM, Display, etc.)
|
||||
├── webapp/
|
||||
│ ├── app.py # Flask application (~1600 lines)
|
||||
│ ├── requirements.txt # Python deps (flask, lxml)
|
||||
│ ├── static/
|
||||
│ │ ├── ge-aerospace-logo.svg # GE Aerospace branding
|
||||
│ │ ├── favicon.ico # Browser favicon
|
||||
│ │ ├── app.js # Frontend JavaScript
|
||||
│ │ ├── bootstrap.min.css # Bootstrap 5 (bundled offline)
|
||||
│ │ ├── bootstrap.bundle.min.js
|
||||
│ │ ├── bootstrap-icons.min.css
|
||||
│ │ └── fonts/ # Icon fonts (woff/woff2)
|
||||
│ └── templates/
|
||||
│ ├── base.html # Layout with GE branding and sidebar nav
|
||||
│ ├── dashboard.html # Service status and overview
|
||||
│ ├── import.html # USB image import wizard
|
||||
│ ├── unattend_editor.html # XML editor for unattend files
|
||||
│ ├── startnet_editor.html # startnet.cmd WIM editor
|
||||
│ ├── image_config.html # Per-image driver and hardware config
|
||||
│ ├── enrollment.html # GCCH bulk enrollment packages
|
||||
│ ├── backups.html # Clonezilla backup management
|
||||
│ ├── reports.html # Blancco erasure reports
|
||||
│ └── audit.html # Activity audit log
|
||||
├── docs/
|
||||
│ └── shopfloor-display-imaging-guide.md # End-user imaging guide
|
||||
├── boot-tools/ # Extracted boot tool files (gitignored)
|
||||
│ ├── blancco/ # Blancco Drive Eraser
|
||||
│ ├── clonezilla/ # Clonezilla Live
|
||||
│ └── memtest/ # Memtest86+
|
||||
├── boot-files/ # WinPE boot files (boot.wim, wimboot, ipxe.efi, BCD)
|
||||
├── offline-packages/ # .deb files (gitignored, built by download-packages.sh)
|
||||
├── pip-wheels/ # Python wheels (gitignored, built by download-packages.sh)
|
||||
├── enrollment/ # PPKGs and run-enrollment.ps1 (gitignored)
|
||||
├── bios-staging/ # Dell BIOS update binaries (gitignored)
|
||||
├── scripts/ # Build, deploy, and helper scripts
|
||||
│ ├── build-usb.sh # Builds the installer USB (2-partition)
|
||||
│ ├── build-proxmox-iso.sh # Builds self-contained Proxmox installer ISO
|
||||
│ ├── prepare-boot-tools.sh # Extracts and patches boot tool files
|
||||
│ ├── download-packages.sh # Downloads offline .debs + pip wheels
|
||||
│ ├── download-drivers.py # Downloads Dell drivers directly from dell.com
|
||||
│ ├── deploy-bios.sh # Pushes BIOS updates to enrollment share
|
||||
│ ├── pull-bios.sh # Pulls BIOS binaries from upstream cache
|
||||
│ ├── sync_hardware_models.py # Syncs hardware model configs across images
|
||||
│ ├── Upload-Image.ps1 # Windows: upload MCL cache to PXE via SMB
|
||||
│ └── Download-Drivers.ps1 # Windows: download hardware drivers from GE CDN
|
||||
├── config/ # Site-specific configuration overrides
|
||||
├── startnet-template.cmd # startnet.cmd template (synced with playbook copy)
|
||||
├── README.md # This file
|
||||
└── SETUP.md # Detailed setup guide
|
||||
```
|
||||
|
||||
## Proxmox Deployment
|
||||
|
||||
A single ISO can be built for deploying the PXE server in a Proxmox VM:
|
||||
|
||||
### Build the ISO
|
||||
|
||||
```bash
|
||||
# Prerequisites (on build workstation)
|
||||
sudo apt install xorriso p7zip-full
|
||||
|
||||
# Build the installer ISO
|
||||
./scripts/build-proxmox-iso.sh /path/to/ubuntu-24.04-live-server-amd64.iso
|
||||
```
|
||||
|
||||
This creates `pxe-server-proxmox.iso` containing the Ubuntu installer, autoinstall config, all offline packages, the Ansible playbook, webapp, and boot tools.
|
||||
|
||||
### Deploy on Proxmox
|
||||
|
||||
1. Upload `pxe-server-proxmox.iso` to Proxmox storage (Datacenter -> Storage -> ISO Images)
|
||||
2. Create a new VM:
|
||||
- **OS:** Linux 6.x kernel
|
||||
- **BIOS:** OVMF (UEFI) or SeaBIOS
|
||||
- **Memory:** 4096 MB
|
||||
- **CPU:** 2+ cores
|
||||
- **Disk:** 40+ GB (VirtIO SCSI)
|
||||
- **Network:** Bridge connected to your isolated PXE network
|
||||
3. Attach the ISO as CD-ROM and start the VM
|
||||
4. Ubuntu auto-installs with zero interaction (~10-15 minutes)
|
||||
5. After reboot, first-boot configures all PXE services automatically
|
||||
6. Access the web interface at `http://172.16.9.1:9009`
|
||||
|
||||
### Import WinPE Images
|
||||
|
||||
After the server is running, import deployment images via the web interface at `http://172.16.9.1:9009/import` or by mounting a USB drive with WinPE content.
|
||||
|
||||
## Samba Shares
|
||||
|
||||
| Share | Path | Purpose |
|
||||
|-----------------|---------------------------|--------------------------------|
|
||||
| winpeapps | /srv/samba/winpeapps | WinPE deployment images |
|
||||
| clonezilla | /srv/samba/clonezilla | Clonezilla disk backup images |
|
||||
| blancco-reports | /srv/samba/blancco-reports| Blancco erasure reports (auto) |
|
||||
| enrollment | /srv/samba/enrollment | GCCH bulk enrollment packages |
|
||||
| image-upload | /home/pxe/image-upload | Image upload staging area |
|
||||
|
||||
All shares use guest access (no authentication) for ease of use on the isolated network.
|
||||
|
||||
## Blancco Drive Erasure
|
||||
|
||||
Blancco Drive Eraser 7.15.1 boots via a native Ubuntu kernel with a custom initramfs (`blancco-init.sh`) that downloads and mounts the Blancco rootfs over HTTP. XML erasure reports are automatically saved to the PXE server's Samba share (`blancco-reports`). The server supports BMC cloud licensing for Blancco activation over WiFi.
|
||||
|
||||
Reports are viewable and downloadable from the web interface at `http://172.16.9.1:9009/reports`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Run `download-packages.sh` before building USB — it downloads all offline `.deb` packages including wimtools (needed for startnet.cmd editing)
|
||||
- The webapp uses session-based CSRF tokens on all POST forms and API endpoints
|
||||
|
||||
## Commit History
|
||||
|
||||
| Commit | Description |
|
||||
|---------|--------------------------------------------------------------------|
|
||||
| 5791bd1 | Initial project setup: automated PXE server provisioning |
|
||||
| cee4ecd | Add web management UI, offline packages, WinPE consolidation |
|
||||
| f614596 | Fix unattend.xml path to match actual image structure |
|
||||
| e7313c2 | Add multi-boot PXE menu, Clonezilla backups, GE Aerospace branding|
|
||||
| 89b5834 | Add wimtools and startnet.cmd editor for boot.wim modification |
|
||||
| 05dbb7e | Add Blancco erasure reports Samba share and webapp viewer |
|
||||
| ef75839 | Auto-patch Blancco config.img for network report storage |
|
||||
| 92c9b0f | Fix review findings: offline assets, security, audit logging |
|
||||
| 725c8f4 | Change webapp to port 9009, add test VM script |
|
||||
| f3a384f | Add Proxmox ISO builder, CSRF protection, boot-files integration |
|
||||
| 1a5c4f7 | Eliminate USB requirement for WinPE PXE boot |
|
||||
| dd2fec5 | Blancco PXE boot via Ubuntu kernel switch_root |
|
||||
| 86660a0 | Remove UEFI HTTP Boot config from dnsmasq |
|
||||
| 6d0e6ee | BIOS check fix, parallel downloads, shopfloor hardening |
|
||||
| 7616549 | Shopfloor PC type system, webapp enhancements |
|
||||
| b7cd097 | Blancco 7.15.1 upgrade: native kernel boot, BMC cloud licensing |
|
||||
| 163e58a | Fix dnsmasq reboot cron: use /etc/cron.d/ instead of crontab |
|
||||
# GE Aerospace PXE Boot Server
|
||||
|
||||
Automated, air-gapped PXE boot server for deploying GE Aerospace Windows images. Built on Ubuntu 24.04 Server with zero-touch provisioning via autoinstall and Ansible.
|
||||
|
||||
## Overview
|
||||
|
||||
This project provides a complete, repeatable build process for a PXE boot server that serves Windows PE images to client machines on an isolated network. Everything runs offline after initial setup — no internet required on the target server.
|
||||
|
||||
### Boot Chain
|
||||
|
||||
```
|
||||
Client PXE boot (UEFI Secure Boot)
|
||||
-> iPXE (TFTP, Broadcom-signed for Secure Boot)
|
||||
-> iPXE boot menu (HTTP, port 4433)
|
||||
-> User selects boot option:
|
||||
├── Windows PE -> wimboot -> boot.wim -> startnet.cmd -> Samba share -> Image deployment
|
||||
├── Clonezilla -> vmlinuz/initrd -> Disk cloning/imaging
|
||||
├── Blancco -> Native kernel + initramfs -> NIST 800-88 drive erasure
|
||||
(memory test: Dell F12 > Diagnostics)
|
||||
```
|
||||
|
||||
### Services
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|-------------|-----------|------------------------------------------|
|
||||
| dnsmasq | 67/udp | DHCP (172.16.9.10-100, 12h lease) |
|
||||
| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
|
||||
| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
|
||||
| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
|
||||
| Samba | 445/tcp | Deployment content + Clonezilla + Blancco|
|
||||
| Flask Webapp| 9009/tcp | Web management interface |
|
||||
|
||||
### Network
|
||||
|
||||
- **PXE server IP:** `172.16.9.1/24`
|
||||
- **DHCP range:** `172.16.9.10` - `172.16.9.100`
|
||||
- **Firewall:** UFW deny-by-default, only service ports open (22, 67, 69, 80, 445, 4433, 9009)
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
**On your workstation (internet-connected):**
|
||||
- Ubuntu 24.04 (or Linux Mint / similar) for downloading packages
|
||||
- Ubuntu Server 24.04 ISO
|
||||
- GE Aerospace Media Creator LITE (for WinPE images)
|
||||
- USB drive >= 8 GB (32+ GB if bundling WinPE images)
|
||||
|
||||
**GE Access Packages (MyAccess portal):**
|
||||
- EPM Rufus Exception Request
|
||||
- EPM DT Functions
|
||||
- DLP - Encrypted Removable (USB) Long Term Access
|
||||
|
||||
### Step 1: Download Offline Packages
|
||||
|
||||
```bash
|
||||
./scripts/download-packages.sh --iso /path/to/ubuntu-24.04.x-live-server-amd64.iso
|
||||
./scripts/check-updates.py # expect: PASSED, 0 security updates
|
||||
./scripts/fetch-ipxe.sh
|
||||
```
|
||||
|
||||
`fetch-ipxe.sh` downloads the Secure Boot iPXE chain into `boot-files/`, pinned by sha256: the iPXE release
|
||||
tarball (its snponly build becomes `ipxe.efi`) and iPXE's dual-signed shim (Microsoft UEFI CA 2011 + 2023),
|
||||
which every UEFI client boots as `ipxe-shim.efi`. Keep the old Broadcom-signed `boot-files/ipxe.efi` too: the
|
||||
playbook installs it as `ipxe-bcm.efi`, the rollback boot file.
|
||||
|
||||
Downloads all .deb packages and Python wheels for offline installation (~140 MB of debs, ~20 MB of wheels).
|
||||
|
||||
### Step 2: Prepare Boot Tools (optional)
|
||||
|
||||
```bash
|
||||
./scripts/prepare-boot-tools.sh /path/to/blancco.iso
|
||||
```
|
||||
|
||||
Extracts and configures boot tool files (Blancco, Clonezilla with Debian's signed shim for Secure Boot). Automatically patches Blancco's config.img to auto-save erasure reports to the PXE server's Samba share.
|
||||
|
||||
### Step 3: Build the USB
|
||||
|
||||
```bash
|
||||
sudo ./scripts/build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
|
||||
```
|
||||
|
||||
On a Windows PC instead: `scripts/stage-usb-share.sh` here, then
|
||||
`Build-PxeUsb.ps1 -DiskNumber N -NoSiteVars` there (see SETUP.md, "Building
|
||||
the USB on a Windows PC").
|
||||
|
||||
`build-usb.sh` creates a bootable USB with two partitions:
|
||||
- **Partition 1:** Ubuntu Server installer
|
||||
- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
|
||||
|
||||
### Step 4: Install on Target Server
|
||||
|
||||
1. Insert USB into the target machine
|
||||
2. Press F12 and boot from USB
|
||||
3. Ubuntu auto-installs with no interaction
|
||||
4. After reboot, the first-boot script:
|
||||
- Installs all offline .deb packages
|
||||
- Runs the Ansible playbook (configures dnsmasq, Apache, Samba, UFW, webapp)
|
||||
- Configures static IP `172.16.9.1/24`
|
||||
5. Move the server's wired NIC to the isolated PXE switch
|
||||
|
||||
### Step 5: Access the Web Interface
|
||||
|
||||
Open `http://172.16.9.1:9009` from any machine on the isolated network.
|
||||
|
||||
## Web Management Interface
|
||||
|
||||
The Flask webapp (port 9009) provides a browser-based management UI:
|
||||
|
||||
- **Dashboard** — Service status overview, disk usage, connected DHCP clients
|
||||
- **Image Import** — Import WinPE deployment images from USB drives
|
||||
- **Unattend Editor** — Edit Windows unattend.xml files per image type (XML syntax highlighting)
|
||||
- **startnet.cmd Editor** — Modify the startnet.cmd inside boot.wim without Windows (uses wimtools)
|
||||
- **Clonezilla Backups** — Upload, download, and manage disk backup images
|
||||
- **Blancco Reports** — View, download, and manage drive erasure reports (auto-collected via Samba)
|
||||
- **Image Config** — Per-image configuration editor (drivers, OS packages, hardware models)
|
||||
- **Enrollment** — Upload, download, and manage GCCH bulk enrollment packages
|
||||
- **Audit Log** — Activity history for all write operations (imports, edits, deletes)
|
||||
|
||||
### Image Types Supported
|
||||
|
||||
| Image Type | Domain | Description |
|
||||
|----------------------|-----------------|-------------------------|
|
||||
| gea-standard | geaerospace.com | Standard desktop |
|
||||
| gea-engineer | geaerospace.com | Engineering desktop |
|
||||
| gea-shopfloor | geaerospace.com | Shop floor kiosk |
|
||||
| ge-standard | ge.com | Standard desktop |
|
||||
| ge-engineer | ge.com | Engineering desktop |
|
||||
| ge-shopfloor-lockdown| ge.com | Shop floor (locked) |
|
||||
| ge-shopfloor-mce | ge.com | Shop floor (MCE) |
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pxe-server/
|
||||
├── autoinstall/
|
||||
│ ├── user-data # Cloud-init autoinstall config + first-boot script
|
||||
│ └── meta-data # Cloud-init metadata (required, empty)
|
||||
├── playbook/
|
||||
│ ├── pxe_server_setup.yml # Ansible playbook: all server configuration
|
||||
│ ├── inventory.ini # Ansible inventory
|
||||
│ ├── startnet.cmd # WinPE startup script (injected into boot.wim)
|
||||
│ ├── blancco-init.sh # Custom initramfs for Blancco PXE boot
|
||||
│ ├── blancco-preferences.xml # Blancco auto-report preferences
|
||||
│ ├── check-bios.cmd # Pre-imaging BIOS update checker
|
||||
│ ├── FlatUnattendW10-shopfloor.xml # Shopfloor unattend.xml
|
||||
│ └── shopfloor-setup/ # PC type setup scripts (Standard, CMM, Display, etc.)
|
||||
├── webapp/
|
||||
│ ├── app.py # Flask application (~1600 lines)
|
||||
│ ├── requirements.txt # Python deps (flask, lxml)
|
||||
│ ├── static/
|
||||
│ │ ├── ge-aerospace-logo.svg # GE Aerospace branding
|
||||
│ │ ├── favicon.ico # Browser favicon
|
||||
│ │ ├── app.js # Frontend JavaScript
|
||||
│ │ ├── bootstrap.min.css # Bootstrap 5 (bundled offline)
|
||||
│ │ ├── bootstrap.bundle.min.js
|
||||
│ │ ├── bootstrap-icons.min.css
|
||||
│ │ └── fonts/ # Icon fonts (woff/woff2)
|
||||
│ └── templates/
|
||||
│ ├── base.html # Layout with GE branding and sidebar nav
|
||||
│ ├── dashboard.html # Service status and overview
|
||||
│ ├── import.html # USB image import wizard
|
||||
│ ├── unattend_editor.html # XML editor for unattend files
|
||||
│ ├── startnet_editor.html # startnet.cmd WIM editor
|
||||
│ ├── image_config.html # Per-image driver and hardware config
|
||||
│ ├── enrollment.html # GCCH bulk enrollment packages
|
||||
│ ├── backups.html # Clonezilla backup management
|
||||
│ ├── reports.html # Blancco erasure reports
|
||||
│ └── audit.html # Activity audit log
|
||||
├── docs/
|
||||
│ └── shopfloor-display-imaging-guide.md # End-user imaging guide
|
||||
├── boot-tools/ # Extracted boot tool files (gitignored)
|
||||
│ ├── blancco/ # Blancco Drive Eraser
|
||||
│ └── clonezilla/ # Clonezilla Live
|
||||
├── boot-files/ # WinPE boot files (boot.wim, wimboot, ipxe.efi, BCD)
|
||||
├── offline-packages/ # .deb files (gitignored, built by download-packages.sh)
|
||||
├── pip-wheels/ # Python wheels (gitignored, built by download-packages.sh)
|
||||
├── enrollment/ # PPKGs and run-enrollment.ps1 (gitignored)
|
||||
├── bios-staging/ # Dell BIOS update binaries (gitignored)
|
||||
├── scripts/ # Build, deploy, and helper scripts
|
||||
│ ├── build-usb.sh # Builds the installer USB (2-partition)
|
||||
│ ├── Build-PxeUsb.ps1 # Windows: builds the installer USB (one FAT32 partition)
|
||||
│ ├── stage-usb-share.sh # Puts repo + payload on the pxe-images share for it
|
||||
│ ├── prepare-boot-tools.sh # Extracts and patches boot tool files
|
||||
│ ├── download-packages.sh # Downloads offline .debs + pip wheels
|
||||
│ ├── download-drivers.py # Downloads Dell drivers directly from dell.com
|
||||
│ ├── capture-site-content.sh # Pulls site content + fresh payload from a live server
|
||||
│ ├── Upload-Image.ps1 # Windows: upload MCL cache to PXE via SMB
|
||||
│ └── Download-Drivers.ps1 # Windows: download hardware drivers from GE CDN
|
||||
├── config/ # Site-specific configuration overrides
|
||||
├── README.md # This file
|
||||
└── SETUP.md # Detailed setup guide
|
||||
```
|
||||
|
||||
## Proxmox Deployment
|
||||
|
||||
Build a disk image with
|
||||
`scripts/build-usb.sh --site-vars site-vars.yml --image pxe-usb.img auto UBUNTU.iso`
|
||||
and import it on the Proxmox host (`qm importdisk <vmid> pxe-usb.img
|
||||
<storage>`), attached after the system disk in the boot order. The VM needs
|
||||
two NICs: one on an isolated imaging bridge (no other DHCP server) and one
|
||||
on the corp bridge; their MACs go in `site-vars.yml`.
|
||||
|
||||
### Import WinPE Images
|
||||
|
||||
After the server is running, import deployment images via the web interface at `http://172.16.9.1:9009/import` or by mounting a USB drive with WinPE content.
|
||||
|
||||
## Samba Shares
|
||||
|
||||
| Share | Path | Purpose |
|
||||
|-----------------|---------------------------|--------------------------------|
|
||||
| winpeapps | /srv/samba/winpeapps | WinPE deployment images |
|
||||
| clonezilla | /srv/samba/clonezilla | Clonezilla disk backup images |
|
||||
| blancco-reports | /srv/samba/blancco-reports| Blancco erasure reports (auto) |
|
||||
| enrollment | /srv/samba/enrollment | GCCH bulk enrollment packages |
|
||||
| image-upload | /home/pxe/image-upload | Image upload staging area |
|
||||
|
||||
All shares use guest access (no authentication) for ease of use on the isolated network.
|
||||
|
||||
## Blancco Drive Erasure
|
||||
|
||||
Blancco Drive Eraser 7.15.1 boots via a native Ubuntu kernel with a custom initramfs (`blancco-init.sh`) that downloads and mounts the Blancco rootfs over HTTP. XML erasure reports are automatically saved to the PXE server's Samba share (`blancco-reports`). The server supports BMC cloud licensing for Blancco activation over WiFi.
|
||||
|
||||
Reports are viewable and downloadable from the web interface at `http://172.16.9.1:9009/reports`.
|
||||
|
||||
## Notes
|
||||
|
||||
- Run `download-packages.sh --iso <the server ISO>` before building the USB: it builds the offline apt repo (every update for the ISO's base plus the explicit packages, wimtools included) that the installer patches and installs from
|
||||
- The webapp uses session-based CSRF tokens on all POST forms and API endpoints
|
||||
|
||||
## Commit History
|
||||
|
||||
| Commit | Description |
|
||||
|---------|--------------------------------------------------------------------|
|
||||
| 5791bd1 | Initial project setup: automated PXE server provisioning |
|
||||
| cee4ecd | Add web management UI, offline packages, WinPE consolidation |
|
||||
| f614596 | Fix unattend.xml path to match actual image structure |
|
||||
| e7313c2 | Add multi-boot PXE menu, Clonezilla backups, GE Aerospace branding|
|
||||
| 89b5834 | Add wimtools and startnet.cmd editor for boot.wim modification |
|
||||
| 05dbb7e | Add Blancco erasure reports Samba share and webapp viewer |
|
||||
| ef75839 | Auto-patch Blancco config.img for network report storage |
|
||||
| 92c9b0f | Fix review findings: offline assets, security, audit logging |
|
||||
| 725c8f4 | Change webapp to port 9009, add test VM script |
|
||||
| f3a384f | Add Proxmox ISO builder, CSRF protection, boot-files integration |
|
||||
| 1a5c4f7 | Eliminate USB requirement for WinPE PXE boot |
|
||||
| dd2fec5 | Blancco PXE boot via Ubuntu kernel switch_root |
|
||||
| 86660a0 | Remove UEFI HTTP Boot config from dnsmasq |
|
||||
| 6d0e6ee | BIOS check fix, parallel downloads, shopfloor hardening |
|
||||
| 7616549 | Shopfloor PC type system, webapp enhancements |
|
||||
| b7cd097 | Blancco 7.15.1 upgrade: native kernel boot, BMC cloud licensing |
|
||||
| 163e58a | Fix dnsmasq reboot cron: use /etc/cron.d/ instead of crontab |
|
||||
|
||||
551
SETUP.md
551
SETUP.md
@@ -1,234 +1,317 @@
|
||||
# PXE Server Setup Guide
|
||||
|
||||
Automated build process for deploying an Ubuntu-based PXE boot server that hosts GE Aerospace Windows PE images. The entire setup is air-gapped — no internet required on the target machine.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Client PXE boot
|
||||
-> Broadcom signed iPXE (Secure Boot)
|
||||
-> iPXE boot menu (HTTP, port 4433)
|
||||
├── Windows PE -> wimboot -> boot.wim -> startnet.cmd -> Samba -> Image deployment
|
||||
├── Clonezilla -> vmlinuz/initrd -> Disk cloning/imaging
|
||||
├── Blancco -> Native kernel + initramfs -> NIST 800-88 drive erasure (auto-reports)
|
||||
└── Memtest86+ -> Memory diagnostics
|
||||
```
|
||||
|
||||
### Services on the PXE Server
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|-------------|-----------|------------------------------------------|
|
||||
| dnsmasq | 67/udp | DHCP (172.16.9.10-100) |
|
||||
| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
|
||||
| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
|
||||
| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
|
||||
| Samba | 445/tcp | Deployment content + backup + reports |
|
||||
| Flask Webapp| 9009/tcp | Web management interface |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Hardware
|
||||
- Server or PC with >= 8 GB RAM, >= 250 GB disk, one wired NIC
|
||||
- USB thumb drive >= 8 GB (32+ GB if bundling WinPE images)
|
||||
|
||||
### Software (on your workstation)
|
||||
- Ubuntu Server 24.04 ISO — https://ubuntu.com/download/server
|
||||
- Linux workstation (Ubuntu/Mint) for running download and build scripts
|
||||
- GE Aerospace Media Creator LITE (for caching WinPE images)
|
||||
|
||||
### GE Access Packages
|
||||
- EPM Rufus Exception Request
|
||||
- EPM DT Functions
|
||||
- DLP - Encrypted Removable (USB) Long Term Access
|
||||
|
||||
## Setup Process
|
||||
|
||||
### Step 1: Download Offline Packages (one-time, requires internet)
|
||||
|
||||
```bash
|
||||
./scripts/download-packages.sh
|
||||
```
|
||||
|
||||
Downloads all .deb packages (ansible, dnsmasq, apache2, samba, wimtools, etc.) into `offline-packages/` and Python wheels (flask, lxml) into `pip-wheels/`. Approximately 252 packages (~140 MB) + 8 Python wheels.
|
||||
|
||||
**Packages included:**
|
||||
- **Server:** dnsmasq, apache2, samba, ufw, cron
|
||||
- **Automation:** ansible
|
||||
- **Tools:** wimtools, unzip, p7zip-full
|
||||
- **Python:** python3-pip, python3-venv
|
||||
- **Network:** network-manager, wpasupplicant, wireless-tools, linux-firmware
|
||||
|
||||
### Step 2: Prepare Boot Tools (optional)
|
||||
|
||||
```bash
|
||||
./scripts/prepare-boot-tools.sh /path/to/blancco.iso /path/to/clonezilla.zip /path/to/memtest.bin
|
||||
```
|
||||
|
||||
Extracts boot files for Blancco, Clonezilla, and Memtest86+ into the `boot-tools/` directory. Automatically patches Blancco's `config.img` to auto-save erasure reports to the PXE server's Samba share.
|
||||
|
||||
### Step 3: Build the USB
|
||||
|
||||
```bash
|
||||
# Basic — server only (import WinPE images later)
|
||||
sudo ./scripts/build-usb.sh /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
|
||||
|
||||
# With WinPE images bundled (single USB, larger drive needed)
|
||||
sudo ./scripts/build-usb.sh /dev/sdX /path/to/ubuntu-24.04.iso /path/to/winpe-images
|
||||
```
|
||||
|
||||
This creates a bootable USB with:
|
||||
- **Partition 1:** Ubuntu Server installer
|
||||
- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
|
||||
|
||||
### Step 4: Boot the Target Machine
|
||||
|
||||
1. Insert the USB into the target machine
|
||||
2. Press F12 (or vendor boot key) and select the USB
|
||||
3. Ubuntu auto-installs — no interaction needed
|
||||
4. After reboot, the first-boot script installs all .deb packages and runs the Ansible playbook
|
||||
5. PXE services (dnsmasq, Apache, Samba) are configured automatically
|
||||
6. Flask webapp starts on port 9009
|
||||
|
||||
### Step 5: Connect to Isolated Network
|
||||
|
||||
Move the server's wired NIC to the isolated switch for PXE clients.
|
||||
|
||||
### Step 6: Import WinPE Content (if not bundled in Step 3)
|
||||
|
||||
**Option A:** Use the web interface at `http://172.16.9.1:9009` to import from USB.
|
||||
|
||||
**Option B:** Manual copy:
|
||||
```bash
|
||||
sudo mkdir -p /mnt/usb2
|
||||
sudo mount /dev/sdb2 /mnt/usb2
|
||||
sudo cp -r /mnt/usb2/. /srv/samba/winpeapps/gea-standard
|
||||
sudo umount /mnt/usb2
|
||||
```
|
||||
|
||||
## Web Management Interface
|
||||
|
||||
Access at `http://172.16.9.1:9009` from any machine on the isolated network.
|
||||
|
||||
| Page | URL Path | Purpose |
|
||||
|-------------------|-------------|-----------------------------------------------|
|
||||
| Dashboard | / | Service status, disk usage, DHCP clients |
|
||||
| Image Import | /import | Import WinPE images from USB drives |
|
||||
| Unattend Editor | /unattend | Edit Windows unattend.xml per image type |
|
||||
| startnet.cmd | /startnet | Edit startnet.cmd inside boot.wim (wimtools) |
|
||||
| Clonezilla Backups| /backups | Upload/download/manage disk backup images |
|
||||
| Blancco Reports | /reports | View/download drive erasure reports |
|
||||
| Image Config | /images/*/config | Per-image driver, OS, and hardware config |
|
||||
| Enrollment | /enrollment | GCCH bulk enrollment package management |
|
||||
| Audit Log | /audit | Activity history for all write operations |
|
||||
|
||||
## Verification
|
||||
|
||||
1. Connect a test workstation to the isolated switch
|
||||
2. Set Network Boot (PXE) as first boot in BIOS/UEFI
|
||||
3. Boot — the client should pull an IP from 10.9.100.x
|
||||
4. iPXE loads, fetches the boot script from port 4433
|
||||
5. Select an option from the boot menu:
|
||||
- **Windows PE**: Boots via wimboot + boot.wim, maps Samba shares, begins deployment
|
||||
- **Clonezilla**: Boots Clonezilla Live for disk imaging
|
||||
- **Blancco**: Boots Drive Eraser, auto-saves reports to server
|
||||
- **Memtest86+**: Runs memory diagnostics
|
||||
|
||||
## Testing with KVM
|
||||
|
||||
```bash
|
||||
# Download Ubuntu ISO
|
||||
wget -O ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso \
|
||||
https://releases.ubuntu.com/noble/ubuntu-24.04.3-live-server-amd64.iso
|
||||
|
||||
# Launch test VM
|
||||
sudo ./test-vm.sh ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso
|
||||
|
||||
# Watch progress (Ctrl+] to detach)
|
||||
sudo virsh console pxe-test
|
||||
|
||||
# After install: ssh pxe@172.16.9.1 / http://172.16.9.1:9009
|
||||
|
||||
# Clean up
|
||||
sudo ./test-vm.sh --destroy
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pxe-server/
|
||||
├── autoinstall/
|
||||
│ ├── user-data # Cloud-init autoinstall + first-boot script
|
||||
│ └── meta-data # Cloud-init metadata (required, empty)
|
||||
├── playbook/
|
||||
│ ├── pxe_server_setup.yml # Ansible: dnsmasq, Apache, Samba, iPXE, UFW, webapp
|
||||
│ ├── inventory.ini # Ansible inventory
|
||||
│ ├── startnet.cmd # WinPE startup script (injected into boot.wim)
|
||||
│ ├── blancco-init.sh # Custom initramfs for Blancco PXE boot
|
||||
│ ├── blancco-preferences.xml # Blancco auto-report preferences
|
||||
│ ├── check-bios.cmd # Pre-imaging BIOS update checker
|
||||
│ ├── FlatUnattendW10-shopfloor.xml # Shopfloor unattend.xml
|
||||
│ └── shopfloor-setup/ # PC type setup scripts (Standard, CMM, Display, etc.)
|
||||
├── webapp/
|
||||
│ ├── app.py # Flask application (~1600 lines)
|
||||
│ ├── requirements.txt # Python deps (flask, lxml)
|
||||
│ ├── static/ # CSS, JS, fonts, logo (all bundled offline)
|
||||
│ └── templates/ # Jinja2 HTML templates (10 pages)
|
||||
├── docs/
|
||||
│ └── shopfloor-display-imaging-guide.md # End-user imaging guide
|
||||
├── boot-tools/ # Extracted boot files (gitignored, built by prepare-boot-tools.sh)
|
||||
│ ├── blancco/ # Blancco Drive Eraser
|
||||
│ ├── clonezilla/ # Clonezilla Live
|
||||
│ └── memtest/ # Memtest86+
|
||||
├── boot-files/ # WinPE boot files (boot.wim, wimboot, ipxe.efi, BCD)
|
||||
├── offline-packages/ # .deb files (gitignored, built by download-packages.sh)
|
||||
├── pip-wheels/ # Python wheels (gitignored, built by download-packages.sh)
|
||||
├── enrollment/ # PPKGs and run-enrollment.ps1 (gitignored)
|
||||
├── bios-staging/ # Dell BIOS update binaries (gitignored)
|
||||
├── scripts/ # Build, deploy, and helper scripts
|
||||
│ ├── build-usb.sh # Builds the 2-partition installer USB
|
||||
│ ├── build-proxmox-iso.sh # Builds self-contained Proxmox installer ISO
|
||||
│ ├── prepare-boot-tools.sh # Extracts/patches boot tools from ISOs
|
||||
│ ├── download-packages.sh # Downloads all offline packages
|
||||
│ ├── download-drivers.py # Downloads Dell drivers from dell.com
|
||||
│ ├── deploy-bios.sh # Pushes BIOS updates to enrollment share
|
||||
│ ├── pull-bios.sh # Pulls BIOS binaries from upstream cache
|
||||
│ ├── sync_hardware_models.py # Syncs hardware model configs across images
|
||||
│ ├── Upload-Image.ps1 # Windows: upload MCL cache to PXE via SMB
|
||||
│ └── Download-Drivers.ps1 # Windows: download hardware drivers from GE CDN
|
||||
├── config/ # Site-specific configuration overrides
|
||||
├── startnet-template.cmd # startnet.cmd template (synced with playbook copy)
|
||||
├── README.md # Project overview
|
||||
└── SETUP.md # Detailed setup guide (this file)
|
||||
```
|
||||
|
||||
## Image Types
|
||||
|
||||
| Image Type | Domain | Description |
|
||||
|----------------------|-----------------|-------------------------|
|
||||
| gea-standard | geaerospace.com | Standard desktop |
|
||||
| gea-engineer | geaerospace.com | Engineering desktop |
|
||||
| gea-shopfloor | geaerospace.com | Shop floor kiosk |
|
||||
| ge-standard | ge.com | Standard desktop |
|
||||
| ge-engineer | ge.com | Engineering desktop |
|
||||
| ge-shopfloor-lockdown| ge.com | Shop floor (locked) |
|
||||
| ge-shopfloor-mce | ge.com | Shop floor (MCE) |
|
||||
|
||||
## Network Configuration
|
||||
|
||||
- PXE server static IP: `172.16.9.1/24`
|
||||
- DHCP range: `172.16.9.10` - `172.16.9.100`
|
||||
- Lease time: 12 hours
|
||||
- DNS: `8.8.8.8` (passed to clients, not used by server)
|
||||
- Firewall: UFW deny-by-default, allow 67/udp 69/udp 80/tcp 445/tcp 4433/tcp 9009/tcp
|
||||
|
||||
## Samba Shares
|
||||
|
||||
| Share | Path | Purpose |
|
||||
|-----------------|----------------------------|-------------------------------|
|
||||
| winpeapps | /srv/samba/winpeapps | WinPE deployment images |
|
||||
| clonezilla | /srv/samba/clonezilla | Clonezilla disk backup images |
|
||||
| blancco-reports | /srv/samba/blancco-reports | Blancco erasure reports (auto)|
|
||||
| enrollment | /srv/samba/enrollment | GCCH bulk enrollment packages |
|
||||
| image-upload | /home/pxe/image-upload | Image upload staging area |
|
||||
|
||||
All shares use guest access for the isolated network.
|
||||
# PXE Server Setup Guide
|
||||
|
||||
Automated build process for deploying an Ubuntu-based PXE boot server that hosts GE Aerospace Windows PE images. The entire setup is air-gapped — no internet required on the target machine.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Client PXE boot
|
||||
-> Broadcom signed iPXE (Secure Boot)
|
||||
-> iPXE boot menu (HTTP, port 4433)
|
||||
├── Windows PE -> wimboot -> boot.wim -> startnet.cmd -> Samba -> Image deployment
|
||||
├── Clonezilla -> vmlinuz/initrd -> Disk cloning/imaging
|
||||
├── Blancco -> Native kernel + initramfs -> NIST 800-88 drive erasure (auto-reports)
|
||||
(memory test: Dell F12 > Diagnostics)
|
||||
```
|
||||
|
||||
### Services on the PXE Server
|
||||
|
||||
| Service | Port | Purpose |
|
||||
|-------------|-----------|------------------------------------------|
|
||||
| dnsmasq | 67/udp | DHCP (172.16.9.10-100) |
|
||||
| dnsmasq | 69/udp | TFTP (serves ipxe.efi) |
|
||||
| Apache | 80/tcp | HTTP (wimboot, WinPE boot files, proxy) |
|
||||
| Apache | 4433/tcp | iPXE boot script (GetPxeScript.aspx) |
|
||||
| Samba | 445/tcp | Deployment content + backup + reports |
|
||||
| Flask Webapp| 9009/tcp | Web management interface |
|
||||
|
||||
## Prerequisites
|
||||
|
||||
### Hardware
|
||||
- Server or PC with >= 8 GB RAM, >= 250 GB disk, one wired NIC
|
||||
- USB thumb drive >= 8 GB (32+ GB if bundling WinPE images)
|
||||
|
||||
### Software (on your workstation)
|
||||
- Ubuntu Server 24.04 ISO — https://ubuntu.com/download/server
|
||||
- Linux workstation (Ubuntu/Mint) for running download and build scripts
|
||||
- GE Aerospace Media Creator LITE (for caching WinPE images)
|
||||
|
||||
### GE Access Packages
|
||||
- EPM Rufus Exception Request
|
||||
- EPM DT Functions
|
||||
- DLP - Encrypted Removable (USB) Long Term Access
|
||||
|
||||
## Setup Process
|
||||
|
||||
### Step 1: Download Offline Packages (one-time, requires internet)
|
||||
|
||||
```bash
|
||||
./scripts/download-packages.sh --iso /path/to/ubuntu-24.04.x-live-server-amd64.iso
|
||||
./scripts/check-updates.py # expect: PASSED, 0 security updates
|
||||
./scripts/fetch-ipxe.sh
|
||||
```
|
||||
|
||||
`fetch-ipxe.sh` downloads the Secure Boot iPXE chain into `boot-files/`, pinned by sha256: the iPXE release
|
||||
tarball (its snponly build becomes `ipxe.efi`) and iPXE's dual-signed shim (Microsoft UEFI CA 2011 + 2023),
|
||||
which every UEFI client boots as `ipxe-shim.efi`. Keep the old Broadcom-signed `boot-files/ipxe.efi` too: the
|
||||
playbook installs it as `ipxe-bcm.efi`, the rollback boot file.
|
||||
|
||||
`download-packages.sh` needs docker (no sudo) and the SAME server ISO that
|
||||
`build-usb.sh` will use. The install is offline (autoinstall uses a dead
|
||||
mirror on purpose), so the server's base is exactly that ISO's; the script
|
||||
unpacks the ISO's install image into a docker image and resolves there:
|
||||
every update of an installed package, the explicit package list
|
||||
(ansible, dnsmasq, apache2, samba, wimtools, zip, the HWE kernel, ...) and
|
||||
the newest version of what the installer adds from the ISO's pool (kernel,
|
||||
grub, openssh-server). `offline-packages/` becomes an apt repository
|
||||
(debs + `Packages` + `explicit.list`), checked offline in a fresh container
|
||||
of the base before the script finishes. On the server, autoinstall's
|
||||
late-commands run `scripts/offline-apt-install.sh` on it: one apt
|
||||
transaction from the local repo only, any error fails the install (log:
|
||||
`/var/log/offline-apt-install.log`). The webapp's pinned, hashed wheels go
|
||||
to `pip-wheels/`. About 500 debs (~1.6 GB) + 8 wheels.
|
||||
|
||||
**Packages included:**
|
||||
- **Server:** dnsmasq, apache2, samba, ufw, cron
|
||||
- **Automation:** ansible
|
||||
- **Tools:** wimtools, unzip, p7zip-full
|
||||
- **Python:** python3-pip, python3-venv
|
||||
- **Network:** network-manager, wpasupplicant, wireless-tools, linux-firmware
|
||||
|
||||
### Step 2: Prepare Boot Tools (optional)
|
||||
|
||||
```bash
|
||||
./scripts/prepare-boot-tools.sh /path/to/blancco.iso
|
||||
```
|
||||
|
||||
Extracts boot files for Blancco and Clonezilla (64-bit with Debian's signed shim, so it boots with Secure Boot on, plus the last 32-bit release for XP-era PCs) into the `boot-tools/` directory. Automatically patches Blancco's `config.img` to auto-save erasure reports to the PXE server's Samba share.
|
||||
|
||||
### Step 2b: Capture site content from a running server
|
||||
|
||||
```bash
|
||||
./scripts/capture-site-content.sh # dry run: lists what would change
|
||||
./scripts/capture-site-content.sh --apply # pull it
|
||||
```
|
||||
|
||||
Pulls what neither the repo nor the MCL image import brings, from a working PXE server (default 172.16.9.1, SSH key login as `pxe`): `_shared/Applications/extra` (Office, Adobe, OpenText, Java, fonts, Wi-Fi, Zscaler), the GE CA certificates and the Engineering Layer payload in each image's `Deploy/Applications`, and the links images keep there, into `site-content/`. It also refreshes `bios-staging/` (BIOS files and `models.txt`), `boot-files/boot.wim` and the Blancco files in `boot-tools/blancco/` from the server. It writes `site-content/SHA256SUMS`, which the playbook checks before restoring anything, and refuses content a FAT32 USB cannot hold. Without it a new server cannot build Standard or Engineer (`build-usb.sh` refuses unless you pass `--skip-site-content`).
|
||||
|
||||
The first MCL image import on the new server then seeds `_shared/Applications/BPRT`, adding our overlay scripts from the repo.
|
||||
|
||||
### Step 3: Build the USB
|
||||
|
||||
```bash
|
||||
# Basic: server only (import WinPE images later). site-vars.yml holds the
|
||||
# two NIC MACs (copy playbook/site-vars.example.yml); --site-vars or
|
||||
# --no-site-vars is required.
|
||||
sudo ./scripts/build-usb.sh --site-vars site-vars.yml /dev/sdX /path/to/ubuntu-24.04-live-server-amd64.iso
|
||||
|
||||
# With WinPE images bundled (single USB, larger drive needed; no file may
|
||||
# be 4 GiB or more, FAT32)
|
||||
sudo ./scripts/build-usb.sh --site-vars site-vars.yml /dev/sdX /path/to/ubuntu-24.04.iso /path/to/winpe-images
|
||||
|
||||
# Same layout into a disk-image file, no root (dd it to a stick later, or
|
||||
# boot it in a VM: see tests/vm/README.md; on Proxmox: qm importdisk).
|
||||
# SIZE is e.g. 16G or "auto".
|
||||
./scripts/build-usb.sh --site-vars site-vars.yml --image pxe-usb.img auto /path/to/ubuntu-24.04.iso
|
||||
```
|
||||
|
||||
Options: `--payload-root DIR` takes offline-packages/, pip-wheels/,
|
||||
boot-files/, boot-tools/, enrollment/, bios-staging/ and drivers-staging/
|
||||
from another checkout; `--skip-enrollment`, `--skip-bios`,
|
||||
`--skip-drivers`, `--skip-boot-tools` leave those out; `--yes` skips the
|
||||
prompt. The build refuses payloads that do not match their pins: the iPXE
|
||||
files against the playbook's `ipxe_*_sha256`, each wheel against its own
|
||||
package's hashes in `webapp/requirements.txt` (no PyPI fallback), and the
|
||||
ISO against the volume id `offline-packages/iso-volid` was resolved for. The ISO volume label and El Torito load sizes are read from the
|
||||
ISO, so any 24.04.x point release works.
|
||||
|
||||
This creates a bootable USB with:
|
||||
- **Partition 1:** Ubuntu Server installer
|
||||
- **Partition 2:** CIDATA (autoinstall config, offline .debs, pip wheels, Ansible playbook, webapp, boot tools)
|
||||
|
||||
#### Building the USB on a Windows PC
|
||||
|
||||
`scripts/Build-PxeUsb.ps1` builds the same stick on Windows, with the same
|
||||
checks, no Linux tools needed. It writes ONE FAT32 partition labelled
|
||||
CIDATA holding the Ubuntu ISO's files (boot menu patched for autoinstall)
|
||||
and the payload; it boots in UEFI mode only (Secure Boot works) and is at
|
||||
most 32 GB (the FAT32 size Windows formats). Every file is read back and
|
||||
compared by SHA-256.
|
||||
|
||||
On the build PC, put the repo and payload on the share:
|
||||
|
||||
```bash
|
||||
./scripts/stage-usb-share.sh # -> ~/pxe-images/pxe-usb (hard links)
|
||||
```
|
||||
|
||||
On the Windows PC, in an elevated PowerShell (Z: = the pxe-images share):
|
||||
|
||||
```powershell
|
||||
Z:\pxe-usb\scripts\Build-PxeUsb.ps1 # lists USB disks
|
||||
Z:\pxe-usb\scripts\Build-PxeUsb.ps1 -DiskNumber 3 -NoSiteVars # or -SiteVars FILE
|
||||
```
|
||||
|
||||
It refuses a disk that is not on the USB bus or is the boot/system disk,
|
||||
and asks you to type the disk number before erasing it.
|
||||
|
||||
### Step 4: Boot the Target Machine
|
||||
|
||||
1. Insert the USB into the target machine
|
||||
2. Press F12 (or vendor boot key) and select the USB
|
||||
3. Ubuntu auto-installs — no interaction needed
|
||||
4. After reboot, the first-boot script runs the Ansible playbook (the .debs
|
||||
were installed by the installer); it pins the two NICs by MAC
|
||||
5. PXE services (dnsmasq, Apache, Samba) are configured automatically
|
||||
6. Flask webapp starts on port 9009
|
||||
7. If the playbook fails, `/var/lib/pxe-firstboot.failed` and a banner on
|
||||
the console and at SSH login point at `/var/log/first-boot.log`; first
|
||||
boot retries at the next reboot (5 attempts)
|
||||
|
||||
### Step 5: Connect to Isolated Network
|
||||
|
||||
During the install plug in ONLY the imaging switch (GE unplugged). With
|
||||
`pxe_nic_mac` empty in site-vars, the first boot picks the one wired port
|
||||
with a link, checks it is isolated (no GE switch, no 802.1X, no other DHCP
|
||||
server) and pins it. Then plug the GE cable into any other port: it becomes
|
||||
a DHCP client for SSH and the webapp, and dnsmasq never serves it. If the
|
||||
first boot cannot tell which port is the imaging one, the console says so:
|
||||
log in and run `sudo pxe-pick-nic`, pull the imaging cable and plug it back
|
||||
in. Sites that prefer to pin both ports by MAC set `pxe_nic_mac` and
|
||||
`corp_nic_mac` instead; both cables can then be in from the start.
|
||||
|
||||
### Step 6: Import WinPE Content (if not bundled in Step 3)
|
||||
|
||||
**Option A:** Use the web interface at `http://172.16.9.1:9009` to import from USB.
|
||||
|
||||
**Option B:** Manual copy:
|
||||
```bash
|
||||
sudo mkdir -p /mnt/usb2
|
||||
sudo mount /dev/sdb2 /mnt/usb2
|
||||
sudo cp -r /mnt/usb2/. /srv/samba/winpeapps/gea-standard
|
||||
sudo umount /mnt/usb2
|
||||
```
|
||||
|
||||
## Web Management Interface
|
||||
|
||||
Access at `http://172.16.9.1:9009` from any machine on the isolated network.
|
||||
|
||||
| Page | URL Path | Purpose |
|
||||
|-------------------|-------------|-----------------------------------------------|
|
||||
| Dashboard | / | Service status, disk usage, DHCP clients |
|
||||
| Image Import | /import | Import WinPE images from USB drives |
|
||||
| Unattend Editor | /unattend | Edit Windows unattend.xml per image type |
|
||||
| startnet.cmd | /startnet | Edit startnet.cmd inside boot.wim (wimtools) |
|
||||
| Clonezilla Backups| /backups | Upload/download/manage disk backup images |
|
||||
| Blancco Reports | /reports | View/download drive erasure reports |
|
||||
| Image Config | /images/*/config | Per-image driver, OS, and hardware config |
|
||||
| Enrollment | /enrollment | GCCH bulk enrollment package management |
|
||||
| Audit Log | /audit | Activity history for all write operations |
|
||||
|
||||
## Verification
|
||||
|
||||
1. Connect a test workstation to the isolated switch
|
||||
2. Set Network Boot (PXE) as first boot in BIOS/UEFI
|
||||
3. Boot — the client should pull an IP from 10.9.100.x
|
||||
4. iPXE loads, fetches the boot script from port 4433
|
||||
5. Select an option from the boot menu:
|
||||
- **Windows PE**: Boots via wimboot + boot.wim, maps Samba shares, begins deployment
|
||||
- **Clonezilla**: Boots Clonezilla Live for disk imaging
|
||||
- **Blancco**: Boots Drive Eraser, auto-saves reports to server
|
||||
- **Memory test**: not on the menu; Dell PCs have one built in (F12 at power-on, Diagnostics)
|
||||
|
||||
## Testing with KVM
|
||||
|
||||
```bash
|
||||
# Download Ubuntu ISO
|
||||
wget -O ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso \
|
||||
https://releases.ubuntu.com/noble/ubuntu-24.04.3-live-server-amd64.iso
|
||||
|
||||
# Launch test VM
|
||||
sudo ./test-vm.sh ~/Downloads/ubuntu-24.04.3-live-server-amd64.iso
|
||||
|
||||
# Watch progress (Ctrl+] to detach)
|
||||
sudo virsh console pxe-test
|
||||
|
||||
# After install: ssh pxe@172.16.9.1 / http://172.16.9.1:9009
|
||||
|
||||
# Clean up
|
||||
sudo ./test-vm.sh --destroy
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
pxe-server/
|
||||
├── autoinstall/
|
||||
│ ├── user-data # Cloud-init autoinstall + first-boot script
|
||||
│ └── meta-data # Cloud-init metadata (required, empty)
|
||||
├── playbook/
|
||||
│ ├── pxe_server_setup.yml # Ansible: dnsmasq, Apache, Samba, iPXE, UFW, webapp
|
||||
│ ├── inventory.ini # Ansible inventory
|
||||
│ ├── startnet.cmd # WinPE startup script (injected into boot.wim)
|
||||
│ ├── blancco-init.sh # Custom initramfs for Blancco PXE boot
|
||||
│ ├── blancco-preferences.xml # Blancco auto-report preferences
|
||||
│ ├── check-bios.cmd # Pre-imaging BIOS update checker
|
||||
│ ├── FlatUnattendW10-shopfloor.xml # Shopfloor unattend.xml
|
||||
│ └── shopfloor-setup/ # PC type setup scripts (Standard, CMM, Display, etc.)
|
||||
├── webapp/
|
||||
│ ├── app.py # Flask application (~1600 lines)
|
||||
│ ├── requirements.txt # Python deps (flask, lxml)
|
||||
│ ├── static/ # CSS, JS, fonts, logo (all bundled offline)
|
||||
│ └── templates/ # Jinja2 HTML templates (10 pages)
|
||||
├── docs/
|
||||
│ └── shopfloor-display-imaging-guide.md # End-user imaging guide
|
||||
├── boot-tools/ # Extracted boot files (gitignored, built by prepare-boot-tools.sh)
|
||||
│ ├── blancco/ # Blancco Drive Eraser
|
||||
│ └── clonezilla/ # Clonezilla Live
|
||||
├── boot-files/ # WinPE boot files (boot.wim, wimboot, ipxe.efi, BCD)
|
||||
├── offline-packages/ # .deb files (gitignored, built by download-packages.sh)
|
||||
├── pip-wheels/ # Python wheels (gitignored, built by download-packages.sh)
|
||||
├── enrollment/ # PPKGs and run-enrollment.ps1 (gitignored)
|
||||
├── bios-staging/ # Dell BIOS update binaries (gitignored)
|
||||
├── scripts/ # Build, deploy, and helper scripts
|
||||
│ ├── build-usb.sh # Builds the 2-partition installer USB
|
||||
│ ├── Build-PxeUsb.ps1 # Windows: builds the installer USB (one FAT32 partition)
|
||||
│ ├── stage-usb-share.sh # Puts repo + payload on the pxe-images share for it
|
||||
│ ├── prepare-boot-tools.sh # Extracts/patches boot tools from ISOs
|
||||
│ ├── download-packages.sh # Downloads all offline packages
|
||||
│ ├── download-drivers.py # Downloads Dell drivers from dell.com
|
||||
│ ├── capture-site-content.sh # Pulls site content + fresh payload from a live server
|
||||
│ ├── Upload-Image.ps1 # Windows: upload MCL cache to PXE via SMB
|
||||
│ └── Download-Drivers.ps1 # Windows: download hardware drivers from GE CDN
|
||||
├── config/ # Site-specific configuration overrides
|
||||
├── README.md # Project overview
|
||||
└── SETUP.md # Detailed setup guide (this file)
|
||||
```
|
||||
|
||||
## Image Types
|
||||
|
||||
| Image Type | Domain | Description |
|
||||
|----------------------|-----------------|-------------------------|
|
||||
| gea-standard | geaerospace.com | Standard desktop |
|
||||
| gea-engineer | geaerospace.com | Engineering desktop |
|
||||
| gea-shopfloor | geaerospace.com | Shop floor kiosk |
|
||||
| ge-standard | ge.com | Standard desktop |
|
||||
| ge-engineer | ge.com | Engineering desktop |
|
||||
| ge-shopfloor-lockdown| ge.com | Shop floor (locked) |
|
||||
| ge-shopfloor-mce | ge.com | Shop floor (MCE) |
|
||||
|
||||
## Network Configuration
|
||||
|
||||
- PXE server static IP: `172.16.9.1/24`
|
||||
- DHCP range: `172.16.9.10` - `172.16.9.100`
|
||||
- Lease time: 12 hours
|
||||
- DNS: `8.8.8.8` (passed to clients, not used by server)
|
||||
- Firewall: UFW deny-by-default, allow 67/udp 69/udp 80/tcp 445/tcp 4433/tcp 9009/tcp
|
||||
|
||||
## Samba Shares
|
||||
|
||||
| Share | Path | Purpose |
|
||||
|-----------------|----------------------------|-------------------------------|
|
||||
| winpeapps | /srv/samba/winpeapps | WinPE deployment images |
|
||||
| clonezilla | /srv/samba/clonezilla | Clonezilla disk backup images |
|
||||
| blancco-reports | /srv/samba/blancco-reports | Blancco erasure reports (auto)|
|
||||
| enrollment | /srv/samba/enrollment | GCCH bulk enrollment packages |
|
||||
| image-upload | /home/pxe/image-upload | Image upload staging area |
|
||||
|
||||
All shares use guest access for the isolated network.
|
||||
|
||||
@@ -9,19 +9,40 @@ autoinstall:
|
||||
variant: ""
|
||||
timezone: America/New_York
|
||||
|
||||
# Network: static IP for isolated PXE LAN (no internet/DHCP needed)
|
||||
# Network: DHCP only, on every wired NIC. On the standard two-NIC box the
|
||||
# GE corporate NIC gets an address and the imaging NIC (no DHCP server on
|
||||
# the imaging LAN) stays without one. No static 172.16.9.1 here: matched
|
||||
# by name it would land on EVERY NIC, the GE one included, and the box
|
||||
# would answer as 172.16.9.1 on the corporate network. First boot's
|
||||
# playbook replaces this file with the imaging NIC pinned by MAC
|
||||
# (pxe_nic_mac in playbook/site-vars.yml, or picked by its link when
|
||||
# empty; corp_nic_mac optional). optional: the
|
||||
# install and boot never wait for a lease (air-gapped is fine).
|
||||
network:
|
||||
version: 2
|
||||
ethernets:
|
||||
any-eth:
|
||||
match:
|
||||
name: "en*"
|
||||
addresses:
|
||||
- 172.16.9.1/24
|
||||
dhcp4: false
|
||||
dhcp4: true
|
||||
dhcp6: false
|
||||
optional: true
|
||||
|
||||
# The install is always offline, even when the corp NIC has a lease and
|
||||
# internet: the mirror points at a closed local port, so subiquity's
|
||||
# mirror check fails and it installs from the ISO alone (no "security
|
||||
# updates" step from the archive). The base is then exactly the ISO's,
|
||||
# which is what download-packages.sh resolved the offline set against;
|
||||
# late-commands patch it from CIDATA. The real archive sources are
|
||||
# written back at the end of late-commands.
|
||||
apt:
|
||||
geoip: false
|
||||
preserve_sources_list: false
|
||||
primary:
|
||||
- arches: [default]
|
||||
uri: "http://127.0.0.1:9/ubuntu"
|
||||
fallback: offline-install
|
||||
|
||||
# Storage configuration
|
||||
storage:
|
||||
layout:
|
||||
@@ -44,34 +65,63 @@ autoinstall:
|
||||
|
||||
# Installer-stage late commands
|
||||
late-commands:
|
||||
# Step 1: Install deb packages from CIDATA USB
|
||||
# Step 1: Upgrade and install from the offline set on CIDATA
|
||||
# (packages/ is an apt repo: debs + Packages + explicit.list). Any apt
|
||||
# error fails the install; the log is /var/log/offline-apt-install.log.
|
||||
- |
|
||||
curtin in-target --target=/target -- bash -c '
|
||||
curtin in-target --target=/target -- bash -euc '
|
||||
mkdir -p /mnt/cidata
|
||||
CIDATA_DEV=$(blkid -L CIDATA)
|
||||
if [ -n "$CIDATA_DEV" ]; then
|
||||
mount "$CIDATA_DEV" /mnt/cidata
|
||||
if compgen -G "/mnt/cidata/packages/*.deb" > /dev/null; then
|
||||
cp /mnt/cidata/packages/*.deb /tmp/
|
||||
dpkg -i /tmp/*.deb 2>/dev/null || true
|
||||
dpkg -i /tmp/*.deb 2>/dev/null || true
|
||||
dpkg --configure -a 2>/dev/null || true
|
||||
if command -v nmcli >/dev/null; then
|
||||
systemctl enable NetworkManager
|
||||
fi
|
||||
fi
|
||||
umount /mnt/cidata
|
||||
mount -o ro "$CIDATA_DEV" /mnt/cidata
|
||||
set +e
|
||||
bash /mnt/cidata/offline-apt-install.sh /mnt/cidata/packages 2>&1 | tee /var/log/offline-apt-install.log
|
||||
rc=${PIPESTATUS[0]}
|
||||
set -e
|
||||
umount /mnt/cidata
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
echo "offline package install FAILED (rc=$rc), see /var/log/offline-apt-install.log" >&2
|
||||
exit "$rc"
|
||||
fi
|
||||
if command -v nmcli >/dev/null; then
|
||||
systemctl enable NetworkManager
|
||||
fi
|
||||
'
|
||||
|
||||
# Step 2: Disable networkd-wait-online (hangs on air-gapped network)
|
||||
- ln -sf /dev/null /target/etc/systemd/system/systemd-networkd-wait-online.service
|
||||
|
||||
# Step 3: Create first-boot.sh
|
||||
# Step 3: Create first-boot.sh. rc.local runs it on every boot until the
|
||||
# playbook succeeds (at most MAX attempts). A failure leaves
|
||||
# /var/lib/pxe-firstboot.failed and a banner on the console (/etc/issue)
|
||||
# and at SSH login (/etc/motd) pointing at /var/log/first-boot.log.
|
||||
- |
|
||||
curtin in-target --target=/target -- bash -c '
|
||||
cat <<"EOF" > /opt/first-boot.sh
|
||||
#!/bin/bash
|
||||
MAX=5
|
||||
STATE=/var/lib/pxe-firstboot
|
||||
FAILED=/var/lib/pxe-firstboot.failed
|
||||
mkdir -p "$STATE"
|
||||
N=$(( $(cat "$STATE/attempts" 2>/dev/null || echo 0) + 1 ))
|
||||
echo "$N" > "$STATE/attempts"
|
||||
echo "=== first-boot attempt $N of $MAX, $(date -Is) ==="
|
||||
banner_off() {
|
||||
for f in /etc/issue /etc/motd; do
|
||||
[ -f "$f" ] && sed -i "/^### PXE FIRST-BOOT BEGIN/,/^### PXE FIRST-BOOT END/d" "$f"
|
||||
done
|
||||
}
|
||||
banner_on() {
|
||||
banner_off
|
||||
for f in /etc/issue /etc/motd; do
|
||||
{
|
||||
echo "### PXE FIRST-BOOT BEGIN"
|
||||
echo "!!! PXE server setup (first-boot playbook) FAILED: $1"
|
||||
echo "!!! Log: /var/log/first-boot.log Marker: $FAILED"
|
||||
echo "!!! $2"
|
||||
echo "### PXE FIRST-BOOT END"
|
||||
} >> "$f"
|
||||
done
|
||||
}
|
||||
# Expand root LV to full disk BEFORE playbook (playbook copies ~130GB of drivers+PPKGs)
|
||||
ROOT_DEV=$(findmnt -n -o SOURCE /)
|
||||
ROOT_DISK=$(lsblk -n -o PKNAME "$(readlink -f "$ROOT_DEV")" | tail -1)
|
||||
@@ -82,37 +132,79 @@ autoinstall:
|
||||
pvresize "$PV_PART" 2>&1 || true
|
||||
fi
|
||||
lvextend -r -l +100%FREE /dev/mapper/ubuntu--vg-ubuntu--lv 2>&1 || true
|
||||
RC=1
|
||||
WHY="CIDATA (the installer USB) not found"
|
||||
CIDATA_DEV=$(blkid -L CIDATA)
|
||||
if [ -n "$CIDATA_DEV" ]; then
|
||||
mkdir -p /mnt/usb
|
||||
mount "$CIDATA_DEV" /mnt/usb
|
||||
if compgen -G "/mnt/usb/packages/*.deb" > /dev/null; then
|
||||
dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
|
||||
dpkg -i /mnt/usb/packages/*.deb 2>/dev/null || true
|
||||
dpkg --configure -a 2>/dev/null || true
|
||||
if mount "$CIDATA_DEV" /mnt/usb; then
|
||||
if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then
|
||||
cd /mnt/usb/playbook
|
||||
rm -f "$STATE/nic-hint"
|
||||
ansible-playbook -i localhost, -c local pxe_server_setup.yml
|
||||
RC=$?
|
||||
WHY="ansible-playbook exit code $RC"
|
||||
# pxe-pick-nic leaves the reason it could not pick the imaging NIC
|
||||
[ "$RC" -ne 0 ] && [ -s "$STATE/nic-hint" ] && WHY="$(head -1 "$STATE/nic-hint")"
|
||||
cd /
|
||||
else
|
||||
WHY="no playbook/pxe_server_setup.yml on CIDATA"
|
||||
fi
|
||||
umount /mnt/usb
|
||||
else
|
||||
WHY="could not mount CIDATA ($CIDATA_DEV)"
|
||||
fi
|
||||
if [ -f /mnt/usb/playbook/pxe_server_setup.yml ]; then
|
||||
cd /mnt/usb/playbook
|
||||
ansible-playbook -i localhost, -c local pxe_server_setup.yml
|
||||
fi
|
||||
umount /mnt/usb
|
||||
fi
|
||||
sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local
|
||||
if [ "$RC" -eq 0 ]; then
|
||||
echo "=== first-boot: playbook succeeded on attempt $N ==="
|
||||
rm -f "$FAILED"
|
||||
banner_off
|
||||
sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local
|
||||
exit 0
|
||||
fi
|
||||
echo "=== first-boot: FAILED ($WHY), attempt $N of $MAX ==="
|
||||
printf "attempt=%s of %s\nwhen=%s\nreason=%s\nlog=/var/log/first-boot.log\n" "$N" "$MAX" "$(date -Is)" "$WHY" > "$FAILED"
|
||||
if [ "$N" -ge "$MAX" ]; then
|
||||
sed -i "s|^/opt/first-boot.sh.*|# &|" /etc/rc.local
|
||||
banner_on "$WHY (attempt $N of $MAX)" "Gave up retrying. Fix it, then run: sudo /opt/first-boot.sh after rm $STATE/attempts"
|
||||
else
|
||||
banner_on "$WHY (attempt $N of $MAX)" "It runs again at the next boot (fix the cause first, e.g. playbook/site-vars.yml on the USB)."
|
||||
fi
|
||||
exit "$RC"
|
||||
EOF
|
||||
chmod +x /opt/first-boot.sh
|
||||
'
|
||||
|
||||
# Step 4: Create rc.local
|
||||
# Step 4: Create rc.local (the log is appended to, one block per attempt)
|
||||
- |
|
||||
curtin in-target --target=/target -- bash -c '
|
||||
cat <<"EOF" > /etc/rc.local
|
||||
#!/bin/bash
|
||||
/opt/first-boot.sh > /var/log/first-boot.log 2>&1 &
|
||||
/opt/first-boot.sh >> /var/log/first-boot.log 2>&1 &
|
||||
exit 0
|
||||
EOF
|
||||
chmod +x /etc/rc.local
|
||||
'
|
||||
|
||||
# Step 5: Real archive sources for later online maintenance (the
|
||||
# install used a dead mirror on purpose, see apt: above).
|
||||
- |
|
||||
rm -f /target/etc/apt/sources.list.d/*.list /target/etc/apt/sources.list.d/*.curtin.orig
|
||||
echo "# Ubuntu sources have moved to /etc/apt/sources.list.d/ubuntu.sources" > /target/etc/apt/sources.list
|
||||
cat > /target/etc/apt/sources.list.d/ubuntu.sources <<"EOF"
|
||||
Types: deb
|
||||
URIs: http://archive.ubuntu.com/ubuntu/
|
||||
Suites: noble noble-updates noble-backports
|
||||
Components: main restricted universe multiverse
|
||||
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
|
||||
Types: deb
|
||||
URIs: http://security.ubuntu.com/ubuntu/
|
||||
Suites: noble-security
|
||||
Components: main restricted universe multiverse
|
||||
Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg
|
||||
EOF
|
||||
|
||||
user-data:
|
||||
disable_root: false
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ systems.
|
||||
|---------------|-----------|------------------------------------------------------|
|
||||
| dnsmasq | 67/udp | DHCP. Hands out IPs and the next-server (TFTP) info. |
|
||||
| dnsmasq | 69/udp | TFTP. Serves `ipxe.efi` to firmware PXE clients. |
|
||||
| Apache | 80/tcp | HTTP. Serves wimboot, boot.wim, Blancco binaries, Clonezilla, memtest, and proxies the webapp on /. |
|
||||
| Apache | 80/tcp | HTTP. Serves wimboot, boot.wim, Blancco binaries, Clonezilla, and proxies the webapp on /. |
|
||||
| Apache | 4433/tcp | HTTP. Serves `Altiris/iPXE/GetPxeScript.aspx` (the iPXE menu script). |
|
||||
| Samba | 445/tcp | SMB shares. WinPE mounts these to fetch image content during deployment. |
|
||||
| Flask webapp | 9009/tcp | Browser UI for image import, unattend editing, log/report viewing. |
|
||||
@@ -112,9 +112,23 @@ WinPE; the Blancco and Clonezilla utilities require Secure Boot disabled.
|
||||
|
||||
c. Clonezilla Live:
|
||||
iPXE loads vmlinuz + initrd over HTTP, fetches filesystem.squashfs.
|
||||
With Secure Boot on, the Debian-signed kernel starts through Debian's
|
||||
Microsoft-signed shim (clonezilla/shimx64.efi, iPXE shim command).
|
||||
The menu checks the CPU (iPXE cpuid --ext 29): 64-bit CPUs get the
|
||||
current release, 32-bit-only CPUs (XP-era Pentium 4/M, early Atom) get
|
||||
clonezilla-i686/ (3.2.0-5, the last 32-bit Clonezilla) over legacy BIOS.
|
||||
"Clonezilla: back up / restore this PC" ask for the machine number,
|
||||
mount \\<server>\clonezilla as the clonezilla Samba user
|
||||
(ocs_repository smb3://, password from clonezilla_smb_password) and
|
||||
run clonezilla-scripts/pxe-cz.sh (ocs_prerun downloads it,
|
||||
ocs_live_run runs it): images are <machine>-<YYYY-MM-DD-HHMM> with
|
||||
pxe-meta.txt, checked after saving; restore lists that machine's
|
||||
images and restores with -k1 -r (larger disks get proportionally
|
||||
larger partitions). "Clonezilla: manual" is the interactive mode.
|
||||
|
||||
d. Memtest86+:
|
||||
iPXE loads memtest.efi.
|
||||
Memory testing is not on the menu: Dell PCs have a built-in test
|
||||
(F12 at power-on, Diagnostics). Memtest86+ was removed because it is
|
||||
unsigned and needed Secure Boot off.
|
||||
```
|
||||
|
||||
## Windows imaging lifecycle (gea-* images)
|
||||
@@ -179,9 +193,7 @@ PXE clients (UEFI -> iPXE -> Blancco) -+--> Drive erasure + report upload
|
||||
## Where things live
|
||||
|
||||
- Ansible playbook: `playbook/pxe_server_setup.yml`. Single file, idempotent.
|
||||
- WinPE startup script (template): `playbook/startnet.cmd` (canonical) and
|
||||
`startnet-template.cmd` (mirror copy at repo root for editor convenience).
|
||||
The playbook injects this into `Windows/System32/startnet.cmd` inside the
|
||||
- WinPE startup script: `playbook/startnet.cmd`. The playbook injects this into `Windows/System32/startnet.cmd` inside the
|
||||
`boot.wim` it deploys to `/var/www/html/win11/sources/`.
|
||||
- iPXE menu script source: embedded in the playbook (`Create GetPxeScript.aspx`
|
||||
task); deployed to `/var/www/html/Altiris/iPXE/GetPxeScript.aspx`.
|
||||
|
||||
95
docs/OWNERSHIP.md
Normal file
95
docs/OWNERSHIP.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Who owns what
|
||||
|
||||
Every bug that cost a day in this pipeline has had the same shape: two systems
|
||||
both setting the same thing, and whichever ran last silently won. This file says
|
||||
who owns each concern so the next person deletes the loser instead of adding a
|
||||
third writer.
|
||||
|
||||
All four examples below are real, from 2026-08-06.
|
||||
|
||||
## Computer name - the PPKG owns it
|
||||
|
||||
The provisioning package declares:
|
||||
|
||||
```xml
|
||||
<DNSComputerName>F%SERIAL%</DNSComputerName>
|
||||
```
|
||||
|
||||
so a bay comes up as `F<serial>`.
|
||||
|
||||
`run-enrollment.ps1` used to also run `Rename-Computer -NewName "E$serial"`.
|
||||
Both are pending renames; last writer wins at reboot. The script ran twice, and
|
||||
its second run landed after the package had already queued `F579C144`, so the
|
||||
bay came up `E579C144` with the package reporting no errors at all.
|
||||
|
||||
**Rule:** nothing in this repo renames a machine. If the naming convention
|
||||
changes, it changes in the package.
|
||||
|
||||
## Drive letters - PESetup owns them
|
||||
|
||||
PESetup hardcodes `W:` in nine places - every copy destination, both DISM
|
||||
offline sessions, `bcdboot`, `reagentc` - and creates it during its own disk
|
||||
preparation.
|
||||
|
||||
`startnet.cmd` briefly had a volume finder that scanned for "the applied
|
||||
Windows volume" and `diskpart`-assigned it to `W:`. On a re-image it found the
|
||||
*previous* install, relabelled a partition PESetup was about to erase, and every
|
||||
staging copy failed into a volume that no longer existed.
|
||||
|
||||
**Rule:** wait for `W:\Windows\System32\config\system` - the hive only exists
|
||||
once the WIM apply has written it. Never run `diskpart` while PESetup is
|
||||
running.
|
||||
|
||||
See `docs/PESETUP-INTERNALS.md`.
|
||||
|
||||
## Enrollment - the PPKG owns it, the orchestrator drives reboots
|
||||
|
||||
The SFLD package joins Entra using the BPRT token in
|
||||
`0__Accounts_Azure.provxml`. A human then assigns the device category in Intune.
|
||||
|
||||
The vendor's `Start-BulkEnrollOrchestrator.ps1` has two branches. The normal one
|
||||
aborts the package's own reboot, registers `AutoSecondReboot`, and drives the
|
||||
Entra join to completion. The `-ManualFallback` one runs `sysprep /oobe /reboot`
|
||||
- it is an interactive escape hatch for handing a machine back to OOBE, meant to
|
||||
be triggered by a person.
|
||||
|
||||
The shopfloor unattend registered `-ManualFallback` as an at-logon scheduled
|
||||
task. So OOBE completed, autologon fired, and four seconds later the machine
|
||||
syspreped itself back to OOBE, losing the deployment chain permanently.
|
||||
|
||||
**Rule:** shopfloor bays enrol. Never wire `-ManualFallback` to anything
|
||||
automatic.
|
||||
|
||||
## Kiosk URLs - GE-Enforce owns them
|
||||
|
||||
`plugins/geenforce/seed_display_scope.py` in shopdb-flask is authoritative:
|
||||
|
||||
| display-type.txt | route |
|
||||
|---|---|
|
||||
| `Dashboard` | `/shopdb/shopfloor` |
|
||||
| `Lobby` | `/shopdb/tv` |
|
||||
| `3DPrintRoom` | `/shopdb/parts-kiosk` |
|
||||
|
||||
Its dispatcher prefers the server-side role from Settings > Dashboard Defaults
|
||||
(resolved by device IP), falls back to `C:\Enrollment\display-type.txt`, writes
|
||||
the Startup shortcut itself, and **sweeps** any shortcut matching
|
||||
`shopfloor-dashboard` or `/shopdb/`.
|
||||
|
||||
`site-config.json` also carries `edgeHomepage` per display type. Those are a
|
||||
backstop for the window before the kiosk installer runs - a stale value there
|
||||
gets deleted on the next enforce cycle rather than honoured.
|
||||
|
||||
**Rule:** if a kiosk points at the wrong page, fix the GE-Enforce scope first.
|
||||
Keep `site-config.json` correct, but do not expect it to win.
|
||||
|
||||
## Repo vs share
|
||||
|
||||
The share is production; the repo is meant to describe it. Drift runs both ways -
|
||||
live hand-edits nobody committed, and repo fixes never deployed.
|
||||
|
||||
`scripts/share-drift.py` classifies every mapped pair as `git-owned` (repo wins,
|
||||
safe to push) or `unreconciled` (diverged, nobody has decided). It reports and
|
||||
never writes. Run it before a build day; `scripts/preflight.py` covers the rest.
|
||||
|
||||
The unattends are `unreconciled` on purpose: the live copies are ~17 KB and the
|
||||
repo copies ~12 KB, so pushing the repo would regress production.
|
||||
278
docs/PESETUP-INTERNALS.md
Normal file
278
docs/PESETUP-INTERNALS.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# PESetup.exe internals
|
||||
|
||||
What GE Image Setup actually does, start to finish. Written from the decompiled
|
||||
assembly, not from observation, because several long-standing beliefs about this
|
||||
tool turned out to be wrong and cost weeks of debugging.
|
||||
|
||||
**Version documented:** 4.0.0.17 (`Sources/PESetup.exe`, PE32+ native apphost
|
||||
wrapping a .NET 6 single-file bundle, 468 embedded files).
|
||||
|
||||
**Version in production:** 4.0.0.20, as of 2026-08-06 — the media reports it in
|
||||
its own log (`AppVersion: 4.0.0.20`). Everything in this document was verified
|
||||
against a 4.0.0.20 run on bay 579C144 that day: the media drive resolved to `Z:\`,
|
||||
`W:` was created by `PrepareDisk` and used for every copy destination, the fallback
|
||||
unattend at `Deploy\FlatUnattendW10.xml` was the one loaded, and driver selection
|
||||
matched `win11_optiplexd13mlk7020_a09.zip` by model. The decompiled detail below has
|
||||
not been re-derived from the 4.0.0.20 binary, so treat exact line-level claims as
|
||||
4.0.0.17 and the observed behaviour as current.
|
||||
|
||||
## How to re-derive this
|
||||
|
||||
The managed code is not directly readable - the outer PE has no managed
|
||||
metadata, so ILSpy refuses it. Extract `PESetup.dll` from the bundle first:
|
||||
|
||||
```python
|
||||
# .NET single-file bundle: signature is SHA-256 of ".net core bundle";
|
||||
# the int64 EIGHT BYTES BEFORE it is the bundle header offset.
|
||||
SIG = bytes([0x8b,0x12,0x02,0xb9,0x6a,0x61,0x20,0x38,
|
||||
0x72,0x7b,0x93,0x02,0x14,0xd7,0xa0,0x32])
|
||||
i = data.find(SIG)
|
||||
header = struct.unpack_from('<q', data, i - 8)[0]
|
||||
# header: uint32 major, uint32 minor, int32 count, 7-bit-prefixed bundle id,
|
||||
# then (v2+) 4 x int64 deps/runtimeconfig, then uint64 flags.
|
||||
# each entry: int64 offset, int64 size, (v6+) int64 compressedSize,
|
||||
# byte type, 7-bit-prefixed path. Compressed entries are raw
|
||||
# deflate (zlib.decompress(raw, -15)).
|
||||
```
|
||||
|
||||
Then `ilspycmd -t <TypeName> PESetup.dll`.
|
||||
|
||||
## Paths it hardcodes
|
||||
|
||||
From `PESetup.Models.GlobalSettings`. All are relative to the MEDIA drive
|
||||
(`Z:\`, whichever winpeapps share startnet mapped) unless stated.
|
||||
|
||||
```
|
||||
ControlDir \Deploy\Control
|
||||
ToolsDir \Deploy\Tools\GE
|
||||
SoftwarePackagesDir \Deploy\Applications
|
||||
HWPackagesDir \Deploy\HW_Apps
|
||||
UnattendFile \Deploy\Tools\GE\XML\FlatUnattendW10.xml
|
||||
UnattendFile2 \Deploy\FlatUnattendW10.xml (fallback)
|
||||
DiskPartDir <exe dir>\DiskPart
|
||||
ConfigSkipPackagesFile skip.json
|
||||
ConfigDisableAutoStartFile disableauto.json
|
||||
ExpirationDuration 30 days
|
||||
AutoStartCountDown 31
|
||||
MinRequiredSpaceWithoutCompression 128849018880 (120 GB)
|
||||
```
|
||||
|
||||
**`W:` IS HARDCODED, EVERYWHERE.** Not derived, not configurable, not a
|
||||
convention this project invented:
|
||||
|
||||
```csharp
|
||||
CopyPackages destDir = "W:\\" + SoftwarePackagesDir
|
||||
CopyHWPackages destDir = "W:\\" + HWPackagesDir
|
||||
CopyTools destDir = "W:\\GE"
|
||||
CopyDrivers destinationPath = "W:\\Drivers\\" + <zip name without extension>
|
||||
ApplyImage destination = "W:\\"
|
||||
CreateBCD "W:\\Windows\\System32\\bcdboot.exe", "W:\\Windows /l en-US"
|
||||
ApplyPackages DismApi.OpenOfflineSession("W:\\")
|
||||
CopyLogs tags -> "w:\\windows\\system32\\"
|
||||
```
|
||||
|
||||
PESetup's own disk preparation is what makes W: exist. Any downstream script
|
||||
that hunts for "the applied volume" is solving a problem this tool does not
|
||||
have. `diskpart list volume` on a machine mid-image shows `Volume 0 W Windows
|
||||
NTFS 237GB Healthy`.
|
||||
|
||||
## The five working steps
|
||||
|
||||
Keys as they appear in the log: `workingstep_gatherdata`, `workingstep_selectos`,
|
||||
`workingstep_prepare`, `workingstep_copy`, `workingstep_apply`,
|
||||
`workingstep_finalize`, `workingstep_reboot`.
|
||||
|
||||
### 1. GATHER DATA
|
||||
|
||||
Four operations, all automatic:
|
||||
|
||||
| Operation | What it establishes |
|
||||
|---|---|
|
||||
| `GatherDataMediaDrive` | `ImageInfo.MediaDrive`, e.g. `Z:\` |
|
||||
| `GatherDataBootDrive` | `ImageInfo.BootDrive`, always `X:\` (WinPE RAM disk) |
|
||||
| `GatherDataSelection` | BIOS version/type, serial, secure boot, OS, languages, model, **driver** |
|
||||
| `GatherDataImageDisk` | picks the physical disk and logs its partitions |
|
||||
|
||||
`GatherDataSelection` is where most decisions are made:
|
||||
|
||||
1. **Secure boot is mandatory.** `SecurebootEnabled != 1` fails the step outright.
|
||||
2. Reads `Control\LanguagePacks.json`.
|
||||
3. `SystemHelpers.GetModel()` and `GetManufacturer()` off WMI.
|
||||
4. Reads `Control\HardwareDriver.json` and `Control\hw_applications.json`.
|
||||
NOTE: it reads **HardwareDriver.json**, not the `hw_drivers.json` that also
|
||||
sits in that folder. Editing the wrong one changes nothing.
|
||||
5. **Virtual platform check.** If ANY driver entry has a manufacturer containing
|
||||
"virtual platform", the tool goes into virtual-only mode: it takes
|
||||
`list[0]` as the driver and then REQUIRES the machine to look virtual
|
||||
(model/serial/BIOS containing VIRTUAL, VMWARE, XEN, QEMU, VirtualBox, vmw).
|
||||
On real hardware that is a hard failure. A stray "virtual platform" entry in
|
||||
the catalogue therefore breaks imaging for every physical machine.
|
||||
6. Otherwise `GetDriverByModel` (below).
|
||||
7. If `Control\skip.json` exists, package installation is skipped.
|
||||
|
||||
### 2. SELECT OS
|
||||
|
||||
Operator-facing. Warns when the media holds an LTSC image ("99% of the time only
|
||||
for ShopFloor"). Also shows days-to-expiry - the media expires 30 days after
|
||||
build (`ExpirationDuration`).
|
||||
|
||||
### 3. PREPARE
|
||||
|
||||
| Operation | What it does |
|
||||
|---|---|
|
||||
| `PrepareDoDisking` | copies `<exedir>\DiskPart\diskpartEFI.txt` to `X:\diskpartEFI<n>.txt`, then `cmd /c DISKPART /S X:\diskpartEFI<n>.txt` |
|
||||
| `PrepareUnattend` | loads the unattend, substitutes, saves to `X:\Unattend.xml` |
|
||||
|
||||
`PrepareUnattend` in detail:
|
||||
|
||||
```csharp
|
||||
text = MediaDrive + UnattendFile; // \Deploy\Tools\GE\XML\FlatUnattendW10.xml
|
||||
if (!File.Exists(text)) text = MediaDrive + UnattendFile2; // \Deploy\FlatUnattendW10.xml
|
||||
xml.Load(text);
|
||||
xml.InnerXml = xml.InnerXml.Replace("%serialnumber%", imageinfo.Serialnumber);
|
||||
xml.InnerXml = xml.InnerXml.Replace("*arch*", arch);
|
||||
SetPackages(arch, xml, imageinfo); // needs Control\Packages.xml
|
||||
xml.Save("X:\\Unattend.xml");
|
||||
```
|
||||
|
||||
Two tokens are substituted: `%serialnumber%` and `*arch*`. `SetPackages` merges
|
||||
in servicing packages from `Control\Packages.xml` and `Control\PackageGroups.xml`.
|
||||
|
||||
The unattend that is USED is the one on the media at `Tools\GE\XML\` if present,
|
||||
otherwise `Deploy\FlatUnattendW10.xml`. A shopfloor variant only takes effect if
|
||||
it occupies one of those two paths.
|
||||
|
||||
### 4. COPY
|
||||
|
||||
Four operations. **None of them filters, and none of them reads the unattend.**
|
||||
|
||||
| Operation | Source | Destination | Rule |
|
||||
|---|---|---|---|
|
||||
| `CopyTools` | `Z:\Deploy\Tools\GE` | `W:\GE` | whole directory, recursive |
|
||||
| `CopyPackages` | `Z:\Deploy\Applications` | `W:\Deploy\Applications` | whole directory, recursive |
|
||||
| `CopyHWPackages` | `Z:\Deploy\HW_Apps` | `W:\Deploy\HW_Apps` | whole directory; **skipped if `IsVirtual` or `HWApps == null`** |
|
||||
| `CopyDrivers` | one `.zip` chosen by model | `W:\Drivers\<zipname>` | **unzipped**, not copied |
|
||||
|
||||
`ExecuteInternalCopy` walks `GetDirectoriesRecursive(source, includeSubDirs:
|
||||
true)` then `Directory.GetFiles(item, "*")`. Every file, every subdirectory. It
|
||||
clears the read-only attribute on each copy.
|
||||
|
||||
**Consequences worth designing around:**
|
||||
|
||||
- Anything dropped into `Deploy\Applications` on the media lands on the target.
|
||||
No manifest, no registration, no unattend reference required.
|
||||
- The copy is FAIL-FAST. One exception on one file returns false and fails the
|
||||
whole step with `copy_packages_error`. A locked or unreadable file in
|
||||
`Applications` fails imaging, it does not get skipped.
|
||||
- Progress is computed from total directory size up front, so bulky additions
|
||||
visibly lengthen this phase.
|
||||
|
||||
### 5. APPLY
|
||||
|
||||
| Operation | What it does |
|
||||
|---|---|
|
||||
| `ApplyImage` | `install.wim` from `OperatingSystem.json`'s `destinationDir`, at `wimindex`, applied to `W:\` |
|
||||
| `ApplyPackages` | DISM offline session on `W:\`, adds servicing packages; logs to `X:\ApplyOSPatch_<n>.log` |
|
||||
| `ApplyLanguagePacks` | same pattern, `X:\ApplyLanguagePack_<n>.log` |
|
||||
| `ApplyUnattend` | DISM offline session on `W:\`, applies `X:\Unattend.xml`; logs to `X:\ApplyUnattend.log` |
|
||||
| `CreateBCD` | `W:\Windows\System32\bcdboot.exe W:\Windows /l en-US` |
|
||||
| `CopyWinRE` | `reagentc /setreimage /path T:\Recovery\WindowsRE /target W:\Windows` |
|
||||
|
||||
All DISM work uses `W:\imagetemp` as scratch and deletes it afterwards.
|
||||
|
||||
### 6. FINALIZE
|
||||
|
||||
`CopyLogs`:
|
||||
- `CopyTagFiles(ToolsDir)` - every `*.tag` from the tools dir to
|
||||
**`w:\windows\system32\`**. This is how build/media tags reach the OS.
|
||||
- `CopyLogFiles("X:\\")` and `CopyXMLFile("X:\\")` - the PESetup log and the
|
||||
generated unattend are preserved.
|
||||
|
||||
## Driver selection, in full
|
||||
|
||||
This is the part most worth understanding, because a miss is nearly silent.
|
||||
|
||||
```csharp
|
||||
// GatherDataSelection
|
||||
List<HardwareDriversRootObject> list =
|
||||
JSONHelpers.ReadJSON_Driver(MediaDrive + ControlDir + "\\HardwareDriver.json");
|
||||
...
|
||||
imageinfo.HWDriver = GetDriverByModel(list, imageinfo.Model);
|
||||
if (imageinfo.HWDriver == null) {
|
||||
// status = Warning, message "driver for [MODEL] not found"
|
||||
return operationResult;
|
||||
}
|
||||
```
|
||||
|
||||
```csharp
|
||||
private static HardwareDriversRootObject? GetDriverByModel(List<...> drivers, string model)
|
||||
{
|
||||
string modelFamily = "";
|
||||
if (model.ToUpper().Contains("LATITUDE")) modelFamily = "Latitude";
|
||||
if (model.ToUpper().Contains("OPTIPLEX")) modelFamily = "Optiplex";
|
||||
if (model.ToUpper().Contains("PRECISION")) modelFamily = "Precision";
|
||||
|
||||
return drivers.Where(d => {
|
||||
if (!string.IsNullOrEmpty(modelFamily) &&
|
||||
!d.family.ToLower().Contains(modelFamily.ToLower())) return false;
|
||||
foreach (string token in d.modelswminame.Split(','))
|
||||
if (model.ToLower().Contains(token.ToLower())) return true; // SUBSTRING
|
||||
return false;
|
||||
}).ToList()?.FirstOrDefault();
|
||||
}
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
```csharp
|
||||
sourceFilePath = MediaDrive + HWDriver.destinationDir.Replace("*destinationdir*","")
|
||||
+ "\\" + HWDriver.fileName;
|
||||
destinationPath = "W:\\Drivers\\" + Path.GetFileNameWithoutExtension(sourceFilePath);
|
||||
StartUnzipAsync(...)
|
||||
```
|
||||
|
||||
### Four traps in that logic
|
||||
|
||||
1. **A miss is a WARNING, not a failure.** Imaging continues and the machine
|
||||
comes up with no drivers - no NIC, no WiFi, so DNS fails and anything
|
||||
network-dependent at first boot fails with it. Symptoms appear far from the
|
||||
cause.
|
||||
2. **Matching is substring, first match wins.** A token like `7020` matches any
|
||||
model string containing 7020. Order in the JSON decides ties.
|
||||
3. **The family filter knows only three Dell lines.** Anything else - MicroPCs,
|
||||
NUCs, non-Dell - skips the filter and depends entirely on
|
||||
`modelswminame` substrings being right.
|
||||
4. **One "virtual platform" entry hijacks the whole catalogue** (see GATHER
|
||||
DATA step 5) and fails every physical machine.
|
||||
|
||||
## What this means for startnet.cmd
|
||||
|
||||
Three beliefs encoded in `startnet.cmd` do not survive contact with the source:
|
||||
|
||||
- *"The applied volume might not be W:."* PESetup hardcodes W: in nine places
|
||||
and creates it during disk prep. The volume finder, the diskpart
|
||||
reassignment and the `W:` wait loop are machinery around a non-problem.
|
||||
- *"We must copy our payload from the enrollment share after imaging."* Anything
|
||||
in `Deploy\Applications` on the media is copied to `W:\Deploy\Applications`
|
||||
by PESetup itself, and is readable at `C:\Deploy\Applications` at first boot.
|
||||
- *"Display MicroPCs failed because the applied volume was not W:."* Worth
|
||||
re-testing. A missing `HardwareDriver.json` match produces the same
|
||||
end-state - no drivers, no network - via a completely different route, and
|
||||
that route only logs a warning.
|
||||
|
||||
## Files PESetup reads from the media
|
||||
|
||||
```
|
||||
Deploy\Control\HardwareDriver.json driver catalogue (NOT hw_drivers.json)
|
||||
Deploy\Control\hw_applications.json per-model applications
|
||||
Deploy\Control\OperatingSystem.json OS list, wim path, wimindex
|
||||
Deploy\Control\LanguagePacks.json language packs
|
||||
Deploy\Control\packages.json servicing packages
|
||||
Deploy\Control\Packages.xml unattend package merge
|
||||
Deploy\Control\PackageGroups.xml package grouping
|
||||
Deploy\Control\Media.tag media identity
|
||||
Deploy\Control\skip.json presence = skip package install
|
||||
Deploy\Control\disableauto.json presence = disable autostart
|
||||
Deploy\Tools\GE\XML\FlatUnattendW10.xml preferred unattend
|
||||
Deploy\FlatUnattendW10.xml fallback unattend
|
||||
```
|
||||
@@ -20,13 +20,67 @@ contribute a `config/sites/<sitename>.yaml` template back to the repo.
|
||||
|
||||
### Network
|
||||
|
||||
The imaging NIC (isolated imaging LAN, static 172.16.9.1, the only NIC that
|
||||
serves DHCP/TFTP/HTTP boot/SMB) is pinned by MAC. The GE corporate side (DHCP
|
||||
client; SSH and the webapp on 9009 only) is optional. Both MACs in
|
||||
`playbook/site-vars.yml` (copy `playbook/site-vars.example.yml`) may be left
|
||||
empty:
|
||||
|
||||
- `pxe_nic_mac` empty: the first boot picks the imaging NIC itself
|
||||
(`pxe-pick-nic --auto`). It takes the one wired port with a link, and only
|
||||
if `pxe-nic-check` calls it isolated, then pins it; the choice is in
|
||||
`/etc/pxe-server/nic-pick.conf` and is never re-guessed at boot. So during
|
||||
the install plug in ONLY the imaging switch. When it cannot tell (no link,
|
||||
two links, or that port sees GE) it changes nothing and the console says
|
||||
to run `sudo pxe-pick-nic`: pull the imaging cable and plug it back in,
|
||||
and the port that blinked is the imaging one. Run it again after swapping
|
||||
a dead USB NIC, then re-run the playbook.
|
||||
- `corp_nic_mac` empty: every other wired port (`en*`, `eth*`) is a DHCP
|
||||
client, so the GE cable can go into any of them, at install or later.
|
||||
dnsmasq is pinned to the imaging NIC (`interface=`) and serves only the
|
||||
imaging subnet, and those ports ignore DHCP offers from 172.16.9.1. UFW
|
||||
allows SSH and 9009 by source, not by interface.
|
||||
- `corp_nic_mac` set: that port alone is the GE NIC, named `corp0` (as on
|
||||
the WJ server).
|
||||
|
||||
The first-boot playbook pins the result in netplan (names `pxe0` / `corp0`,
|
||||
overridable), dnsmasq, UFW and Samba.
|
||||
|
||||
| Value | Default | Where it lives |
|
||||
|-------------------|----------------------|--------------------------------------------------------------------------------|
|
||||
| PXE server IP | 172.16.9.1 | `playbook/pxe_server_setup.yml` (dnsmasq config, iPXE script, samba conf, webapp env), `playbook/startnet.cmd` (mount paths), `boot-tools/blancco/grub-blancco.cfg` (TFTP/HTTP URLs) |
|
||||
| PXE subnet | 172.16.9.0/24 | Same as above, plus `playbook/pxe_server_setup.yml` (UFW rules) |
|
||||
| DHCP range | 172.16.9.10-100 | `playbook/pxe_server_setup.yml` (dnsmasq config) |
|
||||
| DHCP range | 172.16.9.10-100 | `playbook/site-vars.yml` (`pxe_dhcp_start`, `pxe_dhcp_end`) |
|
||||
| NIC MACs / names | (picked at install) / pxe0, corp0 | `playbook/site-vars.yml` (`pxe_nic_mac`, `corp_nic_mac`, `pxe_nic_name`, `corp_nic_name`) |
|
||||
| Hostname | pxeserver | `autoinstall/user-data` (identity.hostname) |
|
||||
|
||||
### Access from the GE corporate network
|
||||
|
||||
SSH (password auth, the shared `pxe` password) and the webapp on 9009 (no
|
||||
login, runs as root) are reachable on the corp NIC. By default from ALL of
|
||||
GE, as before, so a re-run never locks anyone out; the play prints a loud
|
||||
warning on every run while that is so. Set these in `playbook/site-vars.yml`:
|
||||
|
||||
| Var | Default | Effect |
|
||||
|--------------------------|---------|--------|
|
||||
| `corp_admin_cidrs` | `[]` | List of admin subnets/hosts, e.g. `["10.134.48.0/24", "3.12.34.56/32"]`. When non-empty, UFW allows 22 and 9009 from these only (on `corp_nic_name` when `corp_nic_mac` is set, else on any interface) and drops the any-source rules. The play refuses (nothing changed) if an entry is invalid or your own SSH session is not inside one of them. The imaging NIC keeps its rules. |
|
||||
| `corp_ssh_password_auth` | `true` | `false` adds `/etc/ssh/sshd_config.d/10-pxe-corp-auth.conf`: `PasswordAuthentication no`, re-allowed by a `Match Address` block for `pxe_subnet` and localhost. Keys only from GE. The play refuses while `pxe` has no `~/.ssh/authorized_keys`, and removes the drop-in again if `sshd -t` rejects it. |
|
||||
| `allow_disruptive` | `false` | Re-runs while imaging is active (SMB sessions on 445, or DHCP leases granted in the last `pxe_busy_window_min` = 30 min) do not bounce the NICs or restart dnsmasq/Apache/Samba; a needed network change stops the play. `-e allow_disruptive=true` overrides. |
|
||||
|
||||
Recommended for a production box: `corp_admin_cidrs` set to the admin
|
||||
desks' subnet, then (after `ssh-copy-id`) `corp_ssh_password_auth: false`.
|
||||
|
||||
### Re-running the playbook over SSH (from the GE side)
|
||||
|
||||
The network step refuses before changing anything when the NIC that
|
||||
`pxe_nic_mac` names holds the default route, a DHCP lease, or the route to
|
||||
your SSH client (swapped MACs or cables). When it does apply, it arms a
|
||||
120 s auto-revert timer (`pxe-netplan-revert-*`), checks the names, the
|
||||
imaging address and that the corp NIC (or, without one pinned, every other
|
||||
port that had an address) gets its IPv4 back within 30 s, and
|
||||
restores the previous netplan config AND NIC names on any failure. Backups:
|
||||
`/var/backups/netplan-<timestamp>/`.
|
||||
|
||||
### Identity and credentials
|
||||
|
||||
| Value | Default | Where it lives |
|
||||
@@ -44,7 +98,7 @@ credentials in plaintext. These are GE-Aerospace specific.
|
||||
| Value | Default | Where it lives |
|
||||
|---------------|----------------------|-------------------------------------------------------------------------|
|
||||
| WiFi SSID | INTERNETACCESS | `boot-tools/blancco/config.img` (inside the cpio archive: preferences.xml) |
|
||||
| WiFi password | Acc3ssGr@nted22 | Same |
|
||||
| WiFi password | (per site) | Webapp: Updates > Blancco releases > Site settings |
|
||||
| BMC endpoint | classic.eu-west-1.blancco.cloud:443 | Same; encrypted credentials inside config.img |
|
||||
|
||||
To rebuild `config.img` for a different site, see BLANCCO.md ("Rebuilding
|
||||
@@ -64,6 +118,31 @@ The repo defines seven image types (see `playbook/pxe_server_setup.yml`,
|
||||
|
||||
Trim or extend this list and the matching `shopfloor_types` filter as needed.
|
||||
|
||||
### Engineering Layer (gea-engineer)
|
||||
|
||||
Handled by the repo; nothing to work out per site except the payload.
|
||||
|
||||
- `playbook/engineering/Install-EngineeringLayer.ps1` is deployed by the
|
||||
playbook to `gea-engineer/Deploy/Applications/extra_engineering/`.
|
||||
- `FlatUnattendW10-engineer.xml` runs it twice:
|
||||
- specialize Order 37 (`-StageOnly`) copies the payload and the script to
|
||||
`C:\PreInstall\engineering` while `C:\Deploy` exists;
|
||||
- first logon Order 11 installs from that copy. The bulk-enrollment
|
||||
`DeleteDeployDir` task removes `C:\Deploy` on the next boot, so a PC
|
||||
that reboots before the employee signs in would otherwise install
|
||||
nothing and show no icon.
|
||||
- After the install it makes sure the "Configure my Engineering Layer"
|
||||
icon is on the Public desktop (until the PC is configured), and registers
|
||||
Active Setup so a later user gets the icon if someone else already used
|
||||
it. The employee clicks it once, on the GE network: it maps V: and runs
|
||||
`install_local_new.cmd` from GE's share, which removes the icon.
|
||||
- Log on the PC: `C:\Logs\Engineering\install-engineering-layer.log`.
|
||||
|
||||
The site supplies the payload beside the script, in
|
||||
`gea-engineer/Deploy/Applications/extra_engineering/`:
|
||||
`engineering_layer/Engineering_Install_v3.1.exe` and `prereqs/`
|
||||
(.NET 3.5 sxs cabs, VC++ redists), about 650 MB.
|
||||
|
||||
### PC types (shopfloor sub-types)
|
||||
|
||||
`playbook/shopfloor-setup/` contains per-PC-type install logic. The current
|
||||
@@ -80,8 +159,7 @@ A site without CMMs can drop the CMM type and its installers entirely.
|
||||
|
||||
### GCCH bulk-enrollment PPKGs
|
||||
|
||||
PPKG filenames are baked into `playbook/startnet.cmd` and
|
||||
`startnet-template.cmd`. They are tied to the GE Aerospace tenant. Other
|
||||
PPKG filenames are baked into `playbook/startnet.cmd`. They are tied to the GE Aerospace tenant. Other
|
||||
sites/tenants will have their own PPKGs with different filenames.
|
||||
|
||||
| PPKG enrollment menu choice | Default filename |
|
||||
@@ -146,7 +224,9 @@ Blob storage account.
|
||||
- `\\172.16.9.1\image-upload` as the destination
|
||||
- `C:\ProgramData\GEAerospace\MediaCreator\Cache\` as the source
|
||||
|
||||
Update both for a different site.
|
||||
Update both for a different site (`-Server`, `-CachePath`; `-WebPort` if the
|
||||
webapp is not on 9009). The script checks every file on the server through
|
||||
the webapp before the Image Import page will accept the upload.
|
||||
|
||||
## Recommended forward state
|
||||
|
||||
@@ -170,7 +250,7 @@ credentials:
|
||||
|
||||
blancco:
|
||||
wifi_ssid: INTERNETACCESS
|
||||
wifi_password: Acc3ssGr@nted22
|
||||
wifi_password: <per site, set in the webapp>
|
||||
bmc_endpoint: classic.eu-west-1.blancco.cloud:443
|
||||
|
||||
ppkg_versions:
|
||||
|
||||
695
docs/ZERO-TOUCH-IMAGING-PROPOSAL.md
Normal file
695
docs/ZERO-TOUCH-IMAGING-PROPOSAL.md
Normal file
@@ -0,0 +1,695 @@
|
||||
# Zero-touch, ShopDB-driven imaging - proposal
|
||||
|
||||
**Status:** proposal, nothing built
|
||||
**Written:** 2026-08-06
|
||||
**Author's note:** every claim about current behaviour below is from evidence
|
||||
gathered on 2026-08-06 - decompiled binaries, live logs off bay 579C144, and the
|
||||
live share. Where something is inferred rather than observed it says so.
|
||||
|
||||
---
|
||||
|
||||
## 1. What we want
|
||||
|
||||
Two ideas, one architecture.
|
||||
|
||||
**A. The image becomes thin.** It carries only what must happen at image time -
|
||||
partition, apply the WIM, join Entra, install the enforce client. Everything else
|
||||
(PC type, subtype, tool association, apps, settings) is data, pulled at first
|
||||
boot from the ShopDB-Flask API and enforced continuously by GE-Enforce.
|
||||
|
||||
**B. Imaging becomes a job, not a menu.** Open a machine in ShopDB, click
|
||||
*Create this PC*, pick the zeroed box that is sitting in WinPE waiting, and it
|
||||
images start to finish with nobody touching the keyboard - reporting progress
|
||||
onto that machine's record in real time.
|
||||
|
||||
Between them: no menus in WinPE, no per-type media, no decisions baked into text
|
||||
files by whoever happened to be standing at the bay.
|
||||
|
||||
---
|
||||
|
||||
## 2. Why - what today actually costs
|
||||
|
||||
Today every decision is made in WinPE, written to files, and carried through a
|
||||
chain where each link can fail silently:
|
||||
|
||||
```
|
||||
startnet.cmd -> unattend (4 passes) -> FirstLogonCommands (18 orders)
|
||||
-> ppkg -> DSC -> GE-Enforce
|
||||
```
|
||||
|
||||
On 2026-08-06 alone, that chain produced:
|
||||
|
||||
| Failure | Root cause | How long it hid |
|
||||
|---|---|---|
|
||||
| Every build stopped at a dialog | one unattend `Path` at 676 chars against a 259 limit | ~1 day, and it masked the next two |
|
||||
| Bays staged nothing, silently | a volume scan raced PESetup's `diskpart` and staged into a partition being erased | weeks (4 Display bays sat "green") |
|
||||
| Deployment destroyed 4s after autologon | an at-logon task ran `-ManualFallback`, which is `sysprep /oobe /reboot` | until traced through Panther |
|
||||
| Machines named `E<serial>` not `F<serial>` | our script re-applied a rename over the package's own | unknown |
|
||||
| Six copies reported success having moved 0 bytes | unconditional `echo` after `robocopy` | weeks |
|
||||
|
||||
The pattern is identical in every case: **a decision made early, carried far, and
|
||||
verified nowhere.** Moving decisions to a running, networked machine with an API
|
||||
and a UI makes them recoverable - re-runnable, correctable, and observable.
|
||||
|
||||
---
|
||||
|
||||
## 3. What we already have to build on
|
||||
|
||||
This is not a greenfield project. Most of the mechanism exists.
|
||||
|
||||
### 3.1 ShopDB-Flask
|
||||
- Assets keyed by serial; CMM bays, measuring tools, machine numbers already modelled
|
||||
- Collector API: `POST /shopdb/api/collector/computers` (X-API-Key), ingests inventory by serial
|
||||
- GE-Enforce plugin: scopes, manifests, **content-addressed payload store**
|
||||
(`instance/geenforce/payloads/<sha256>`), `GET /api/geenforce/payload/<sha256>`
|
||||
- Display scope (`plugins/geenforce/seed_display_scope.py`) already proves the pattern:
|
||||
read a local subtype, prefer a **server-side role resolved by device IP**, pull
|
||||
payloads over HTTPS, write the kiosk shortcut, sweep stale ones, self-heal each cycle
|
||||
- Per-device role API: `GET /api/dashboarddefaults/display-role`
|
||||
|
||||
### 3.2 PXE server
|
||||
- Per-bay state: `/var/log/pxe-imaging/<serial>.json` with stage history
|
||||
- `winpe-status-push.ps1` already POSTs stage updates from WinPE
|
||||
- Data-driven menus already: `menu.json` on the share, rendered by
|
||||
`select-shopfloor-type.ps1`, edited by the webapp
|
||||
- Imaging dashboard with per-bay tiles
|
||||
- As of today: staging verification, imaging-log harvest to
|
||||
`enrollment/imaging-logs/<serial>/`, `preflight.ps1` at the bay,
|
||||
`preflight.py` + `lint-unattend.py` + `lint-driver-catalogue.py` +
|
||||
`share-drift.py` on the server
|
||||
|
||||
### 3.3 Hardware
|
||||
- Second NIC already present and unused: `enp0s31f6` (onboard, `e1000e`, currently down)
|
||||
- PXE LAN currently on a USB adapter, `enx34c8d6b11010`
|
||||
- That adapter was silently capped at USB 2.0 (327 Mb/s measured); moved to a
|
||||
USB 3 port on 2026-08-06 and now runs at line-rate gigabit (**937 Mb/s measured**)
|
||||
|
||||
---
|
||||
|
||||
## 4. Target architecture
|
||||
|
||||
### 4.1 Three (or one) pre-imaged types
|
||||
|
||||
Proposed: **Displays, Inspection, Shopfloor** as pre-imaged stock, with the
|
||||
subtype chosen later by the user at first boot.
|
||||
|
||||
**Open question worth settling early:** if the base image is identical and all
|
||||
configuration comes from the API, what actually differs between the three?
|
||||
Drivers are selected by PESetup per *model*, not per type. Unless disk layout or
|
||||
a must-be-offline application differs, **one image** is better - three media trees
|
||||
mean three things to keep in sync, and drift is what caused the day-long outage.
|
||||
|
||||
### 4.2 First-boot selection
|
||||
|
||||
A friendly PS1 runs when no PC type is recorded yet:
|
||||
|
||||
```
|
||||
Inspection -> CMM | Genspect | Wax Trace | Keyence
|
||||
└─ CMM / Keyence / WaxTrace -> which measuring tool?
|
||||
(list served from ShopDB assets, not a CSV)
|
||||
Displays -> Lobby | Dashboard | 3D Print Kiosk
|
||||
Shopfloor -> Collections | No Collections | Common | Heattreat | Part Marker | ...
|
||||
```
|
||||
|
||||
The tool selection is the important half: it drives PC-DMIS version, FormTracePak
|
||||
version, DODA flag, NTLARS/eDNC registry, UDC settings, machine number.
|
||||
|
||||
**The network constraint that shapes this.** ShopDB is only reachable once the
|
||||
bay has joined the **AESFMA wifi SSID** - not on the imaging LAN, and not
|
||||
necessarily at first boot. So a first-boot selection UI cannot assume the API is
|
||||
there. It must either work offline and sync later, or wait. Confirmed on
|
||||
579C144: the bay held `172.16.9.81` and `172.24.19.142`, neither in the
|
||||
production ranges, and `sync_intune` sat retrying every 30 seconds.
|
||||
|
||||
**The pattern that solves it, already built and worth reusing.**
|
||||
`Install-ShopdbKiosk-WhenOnline.ps1` is a SYSTEM task armed at imaging that does
|
||||
nothing until ShopDB answers, then acts once, verifies, and unregisters itself.
|
||||
Any step in the new architecture that needs the API - selection sync, config
|
||||
pull, enrollment completion - can use the same shape rather than assuming
|
||||
connectivity.
|
||||
|
||||
**Design points:**
|
||||
|
||||
- **Server is the source of truth.** The selection POSTs to ShopDB keyed on
|
||||
serial; the local file is a cache. A mis-selection is then fixable from a
|
||||
browser, not by walking to the bay.
|
||||
- **Re-runnable.** Available from the Start menu, shows what was chosen and what
|
||||
was installed. People pick wrong; that must not mean re-imaging.
|
||||
- **Audited.** ShopDB records who chose what and when - that is what separates
|
||||
"misconfigured" from "chose wrong" six months later.
|
||||
- **Do not require the LAPS password.** Handing a shop-floor user the local admin
|
||||
password to run a selection dialog is a real control change, and the app runs
|
||||
elevated. Prefer a SYSTEM-run UI on the logon desktop, or a kiosk shell.
|
||||
|
||||
### 4.3 Per-machine configuration records
|
||||
|
||||
Today a machine's identity is scattered:
|
||||
|
||||
| What | Where today | Size |
|
||||
|---|---|---|
|
||||
| eDNC / NTLARS registry | `_ntlars-backups/` - 147 per-bay `.reg` files | ~1.5 MB total |
|
||||
| WaxTrace bay config | `bay-config.csv` - 15 bays, 7 FTPak versions, per-bay user ID | small |
|
||||
| CMM bay config | `cmm-bay-config.csv` -> version.txt, doda.txt | small |
|
||||
| UDC settings | `pre-install/udc-backups/`, per-bay | ~240 MB per bay |
|
||||
| Machine number | `machine-number.txt` written by startnet | trivial |
|
||||
|
||||
Every bay gets **all** of it staged, then picks its own.
|
||||
|
||||
Proposed: one record per machine in ShopDB.
|
||||
|
||||
```
|
||||
ShopDB asset (key: serial and/or machine number)
|
||||
├─ pctype + subtype -> selects the GE-Enforce scope
|
||||
├─ tool association -> CMM bay, Keyence model, FTPak version, DODA
|
||||
├─ machine number -> UDC / eDNC naming
|
||||
└─ payload refs (sha256) -> NTLARS .reg, UDC backup, per-bay settings
|
||||
```
|
||||
|
||||
**Reference payloads, do not embed them.** NTLARS `.reg` files carry binary
|
||||
values and UDC backups are hundreds of MB. GE-Enforce already has a
|
||||
content-addressed blob store with an HTTP fetch endpoint - the record names the
|
||||
payload, the store holds it.
|
||||
|
||||
This also removes a live bug class. `Restore-UDCData.ps1` and
|
||||
`Update-MachineNumber.ps1` each mount SMB backup shares on their own drive
|
||||
letters, which collided with GE-Enforce's `W:` and killed the manifest entry
|
||||
running next (fixed in `66c24b5` by moving them to `R:` and `N:`). Payloads over
|
||||
HTTPS remove drive-letter juggling entirely.
|
||||
|
||||
Migration is a one-time import of two CSVs and 147 `.reg` files. A script, not a
|
||||
project.
|
||||
|
||||
---
|
||||
|
||||
## 5. Zero-touch: ShopDB-triggered imaging
|
||||
|
||||
### 5.1 Flow
|
||||
|
||||
```
|
||||
1. Zeroed PC, boot order = IPv4 only. PXE boots into WinPE.
|
||||
2. WinPE reports serial + MAC + model, then WAITS.
|
||||
3. ShopDB shows it under "awaiting assignment".
|
||||
4. Operator opens the target machine -> "Create this PC" -> picks the waiting box.
|
||||
5. WinPE polls, receives a job:
|
||||
{ pctype, subtype, tool/bay, tenant, purpose, target asset id }
|
||||
and images with NO menus.
|
||||
6. Progress streams to that machine's record in real time.
|
||||
7. On completion the PC binds to the asset record.
|
||||
```
|
||||
|
||||
Every menu in `startnet.cmd` becomes a field in the job. If no job exists, it
|
||||
falls back to today's interactive menu and displays its own serial so one can be
|
||||
created.
|
||||
|
||||
### 5.2 Safety - this is a remote-triggered disk wipe
|
||||
|
||||
Clicking a web button destroys a machine, and the target is chosen from a list.
|
||||
The nightmare is picking the wrong row.
|
||||
|
||||
**Mitigation:** the waiting PC displays a short code on screen; the job cannot be
|
||||
claimed unless that code matches what the operator confirms in ShopDB. Cheap, and
|
||||
it makes wrong-machine wipes structurally hard rather than merely unlikely.
|
||||
|
||||
Additionally: a job may only target a machine currently in the waiting state, and
|
||||
the wipe must be logged against both the operator and the asset.
|
||||
|
||||
### 5.3 Real-time record updates
|
||||
|
||||
Each phase already produces data; it simply is not joined up.
|
||||
|
||||
| Phase | Existing source | Lands on the record as |
|
||||
|---|---|---|
|
||||
| PXE boot | dnsmasq lease + WinPE check-in | serial, MAC, model, "imaging started" |
|
||||
| WIM apply | `winpe-status-push.ps1` | live stage / progress |
|
||||
| Staging | `winpe-staging.log` + harvested bundle | what payload actually landed |
|
||||
| First boot | collector API | CPU, RAM, disk, OS build |
|
||||
| Enrollment | `criticalChecks.json`, `TokenMatch.json` | Entra join state, tenant, purpose |
|
||||
| Steady state | GE-Enforce report | installed apps, drift, self-heal events |
|
||||
|
||||
End state is one timeline per machine: *imaged 14:32 -> staged
|
||||
gea-shopfloor-cmm, bay CMM4 -> enrolled GCCH_SH -> PC-DMIS 2019 R2 installed ->
|
||||
enforcing clean since*.
|
||||
|
||||
**Two rules so it does not rot:**
|
||||
|
||||
1. **Push, best-effort.** If ShopDB is unreachable the bay must still image.
|
||||
Queue events on the PXE server and drain them; the harvested logs are the
|
||||
durable local copy.
|
||||
2. **One writer per field.** Imaging status from the PXE server, inventory from
|
||||
the collector, app state from GE-Enforce. Two writers on one field is the
|
||||
exact bug that ate 2026-08-06.
|
||||
|
||||
---
|
||||
|
||||
## 6. Network design
|
||||
|
||||
### 6.1 Dual-homed PXE server
|
||||
|
||||
- **PXE side** - isolated `172.16.9.0/24`, dnsmasq DHCP bound to that adapter
|
||||
- **GE side** - corporate, reachable from anywhere; carries ShopDB API traffic,
|
||||
the job queue, status pushes, and (if wanted) the BPRT token fetch
|
||||
|
||||
The onboard `enp0s31f6` is free for this today.
|
||||
|
||||
### 6.2 Security prerequisites - these gate the whole thing
|
||||
|
||||
Checked 2026-08-06:
|
||||
|
||||
**Already safe.** dnsmasq has `listen-address=172.16.9.1` + `bind-dynamic`, so it
|
||||
binds only to the interface holding that address. DHCP will **not** leak onto a
|
||||
GE-side NIC. This was the failure I most expected and it is already handled.
|
||||
|
||||
**Not safe yet:**
|
||||
|
||||
```
|
||||
smb.conf no `interfaces =` / `bind interfaces only` -> Samba listens on ALL interfaces
|
||||
ufw every rule is ALLOW IN Anywhere, not interface-scoped:
|
||||
22/tcp SSH (account is pxe/pxe)
|
||||
445/tcp shares (pxe-upload, password in scripts)
|
||||
69/udp TFTP
|
||||
9009,9011 webapp
|
||||
```
|
||||
|
||||
The moment a GE-side interface comes up, all of that is corporate-reachable.
|
||||
|
||||
**Required before dual-homing:**
|
||||
1. Scope every UFW rule to the PXE interface (`ufw allow in on <pxe-if> ...`)
|
||||
2. `interfaces = <pxe-if>` + `bind interfaces only = yes` in `smb.conf`
|
||||
3. Expose deliberately on the GE side only what must be - realistically the
|
||||
webapp, behind real authentication
|
||||
4. Reconsider SSH credentials if the host becomes corporate-reachable
|
||||
|
||||
*(Correction to an earlier note: the shares are **not** guest-accessible -
|
||||
`guest ok = no` on every share, with `map to guest = bad user`. They require the
|
||||
`pxe-upload` account. That account's password is in scripts, so it is not a
|
||||
control once 445 is corporate-reachable.)*
|
||||
|
||||
### 6.3 What dual-homing unlocks
|
||||
|
||||
- Job queue sync and live status without the share as intermediary
|
||||
- BPRT token fetch (`mcl.dwcdn.geaerospace.com`) reachable at imaging time
|
||||
- Possibly the Entra join during imaging rather than after re-cabling - see §8.2
|
||||
|
||||
---
|
||||
|
||||
## 7. The PESetup question
|
||||
|
||||
### 7.1 Autostart does not apply to our media
|
||||
|
||||
`AutoStartCountDown = 31` exists in the decompiled settings, and
|
||||
`disableauto.json` - whose *presence* disables autostart - is absent from the
|
||||
media. On the surface that suggests a 31-second countdown, and the observed log
|
||||
is consistent with an operator simply clicking before it fired:
|
||||
|
||||
```
|
||||
11:30:07.74 'Selection View' started (LTSC warning displayed)
|
||||
11:30:18.63 Btn 'Next' pressed.
|
||||
```
|
||||
|
||||
**However:** per the team's operational experience, **LTSC builds cannot
|
||||
autostart**. The SELECT OS step warns when the media holds an LTSC image
|
||||
("99% of the time only for ShopFloor. Are you sure you want 'LTSC'?") and that
|
||||
confirmation is mandatory regardless of the countdown. Our media is
|
||||
`Windows 11 LTSC 24H2` (`OperatingSystem.json`, build 26100, id 20), so every
|
||||
image we produce hits it.
|
||||
|
||||
Consequence: **the countdown is not a route to zero-touch for us.** The 40-second
|
||||
hands-off test is still worth doing once to confirm the behaviour first-hand and
|
||||
record it - but plan on the answer being "it waits".
|
||||
|
||||
That promotes §7.2 from a contingency to the actual decision.
|
||||
|
||||
### 7.2 Re-implementation - now the likely path, and tractable
|
||||
|
||||
Nothing in the mechanism is proprietary - only the data, which we already own and
|
||||
already parse.
|
||||
|
||||
| PESetup step | Equivalent |
|
||||
|---|---|
|
||||
| `PrepareDisk` | `diskpart /s diskpartEFI.txt` |
|
||||
| `PrepareUnattend` | token substitution (`%serialnumber%`, `*arch*`) + `Packages.xml` merge |
|
||||
| `CopyTools` / `CopyPackages` / `CopyHWPackages` | robocopy |
|
||||
| `CopyDrivers` | select by model from `HardwareDriver.json`, unzip |
|
||||
| `ApplyImage` | `DISM /Apply-Image` per `OperatingSystem.json` |
|
||||
| `ApplyPackages` / `ApplyLanguagePacks` | `DISM /Add-Package` |
|
||||
| `ApplyUnattend` | `DISM /Apply-Unattend` |
|
||||
| `CreateBCD` / `CopyWinRE` | `bcdboot`, `reagentc` |
|
||||
|
||||
A few hundred lines of PowerShell. We would also get to fix the traps documented
|
||||
in `docs/PESETUP-INTERNALS.md`: substring first-match-wins driver matching, a
|
||||
family filter that knows only Latitude/OptiPlex/Precision, and a driver miss
|
||||
being a **warning** rather than a failure.
|
||||
|
||||
**The cost is ownership, not effort.** `Release.json` points at MCL auto-update
|
||||
URLs, so GE expects this media to self-update. Diverging means tracking their
|
||||
format changes indefinitely, plus a likely compliance conversation about the
|
||||
mandated imaging tool. Since the LTSC gate means autostart will not save us
|
||||
(§7.1), this is a decision to take deliberately rather than a fallback.
|
||||
|
||||
### 7.3 What we would keep either way
|
||||
|
||||
Image-time preflight stays valuable regardless: secure boot (PESetup hard-fails
|
||||
without it), >=120 GB disk (`MinRequiredSpaceWithoutCompression`), driver match
|
||||
for the model, media age (30-day expiry). Already built as
|
||||
`playbook/scripts/preflight.ps1`.
|
||||
|
||||
---
|
||||
|
||||
## 8. Enrollment
|
||||
|
||||
### 8.1 How it works today
|
||||
|
||||
- Six MCL packages on the media: `PERS` / `SH` / `LOAN` x `GCCH` / `RoW`, plus
|
||||
the SFLD package for shopfloor
|
||||
- `Start-BulkEnrollOrchestrator.ps1` asks tenant + purpose via
|
||||
`Select-PCConfig.ps1` (a GUI), then applies the matching package
|
||||
- `Insert-BPRTToPPKG.ps1` fetches an **encrypted token table** from
|
||||
`mcl.dwcdn.geaerospace.com`, decrypts with a static passphrase
|
||||
(PBKDF2 310k / SHA-256 / AES-CBC), matches on **Tenant AND Purpose**, injects
|
||||
the token and stamps the real expiry into the filename
|
||||
(`Exp_XXXXXXXX` -> `Exp_yyyyMMdd`)
|
||||
- The package itself sets `<DNSComputerName>F%SERIAL%</DNSComputerName>` and
|
||||
performs the Entra join
|
||||
- A human then assigns the device category in Intune
|
||||
|
||||
As of 2026-08-06 the tenant/purpose choice can be pre-seeded at the PXE menu
|
||||
(`C:\Enrollment\pc-config.txt`, consumed by a shim at the path the orchestrator
|
||||
already calls). Injection deliberately stays at first logon - it needs internet.
|
||||
|
||||
### 8.2 The constraint that shapes pre-imaged stock
|
||||
|
||||
The BPRT token is stamped into the package with an expiry. A PC imaged, powered
|
||||
off, and left in a cupboard for three months has a **dead token** and will never
|
||||
join - and the failure looks identical to "not on the production network yet",
|
||||
because `Entra ID Joined: false` right after imaging is normal.
|
||||
|
||||
Three options, pick deliberately:
|
||||
|
||||
1. **Join at imaging time** - requires the production network at the bench
|
||||
(which dual-homing could provide), and pre-imaged stock is then already joined
|
||||
2. **Join at first power-on** - cleaner logistically, but stock has a shelf life
|
||||
bounded by token expiry
|
||||
3. **Re-inject on demand** - first boot detects a dead token and fetches a fresh
|
||||
one; needs the machine to reach the token blob
|
||||
|
||||
Whichever is chosen, the *silent* failure must be closed: a check that
|
||||
distinguishes "token expired" from "no network yet".
|
||||
|
||||
---
|
||||
|
||||
## 9. Ownership model
|
||||
|
||||
Four times on 2026-08-06 two systems owned one thing and the last writer won
|
||||
silently. A clean slate is the moment to fix that. See `docs/OWNERSHIP.md`.
|
||||
|
||||
| Concern | Owner | Not |
|
||||
|---|---|---|
|
||||
| Computer name | the provisioning package (`F%SERIAL%`) | any script in this repo |
|
||||
| Drive letters during imaging | PESetup (`W:`, hardcoded 9 places) | anything running `diskpart` concurrently |
|
||||
| Entra enrollment | the package + orchestrator | `run-enrollment.ps1` |
|
||||
| Kiosk / display targets | GE-Enforce scope | `site-config.json` (backstop only) |
|
||||
| Imaging status | PXE server | the collector |
|
||||
| Inventory | collector API | the PXE server |
|
||||
| Installed app state | GE-Enforce reports | anything else |
|
||||
| Job queue | **ShopDB** (owns assets) | the PXE webapp (executor only) |
|
||||
|
||||
---
|
||||
|
||||
## 9a. Design rules learned the hard way (2026-08-06)
|
||||
|
||||
A day of fixing the current pipeline produced five rules. They are cheap to
|
||||
honour in a new design and expensive to retrofit.
|
||||
|
||||
### Absent and empty are different
|
||||
|
||||
Four separate scripts tested config as
|
||||
`$null -ne $cfg -and $cfg.Count -gt 0`, so an explicitly empty list fell through
|
||||
to a hardcoded default. A Display kiosk configured with
|
||||
|
||||
```json
|
||||
"desktopApps": [], "startupItems": [], "taskbarPins": []
|
||||
```
|
||||
|
||||
was given UDC, eDNC, NTLARS, WJ Shopfloor, Defect Tracker and Plant Apps -
|
||||
**configuring "none" produced "everything"**. Plant Apps launched
|
||||
`msedge --new-window` on a screen with no keyboard.
|
||||
|
||||
Measured, not assumed: an empty JSON array round-trips as `Object[]` with
|
||||
`Count = 0`, and an absent key as `$null`. They are distinguishable; the code
|
||||
simply conflated them.
|
||||
|
||||
*Rule: in the API model, "no items" and "not configured" must be different
|
||||
values on the wire, and the default for an unrecognised or missing scope is
|
||||
NOTHING, not a fallback set.*
|
||||
|
||||
### Deny by default, always
|
||||
|
||||
`preinstall.json` had 16 of 21 entries at `PCTypes: ["*"]`, so a kiosk installed
|
||||
Adobe, OpenText, Defect Tracker, the serial drivers and twelve legacy VC++
|
||||
redistributables. The SMB manifest had 18 of 25 entries unfiltered. In both
|
||||
cases the filter worked perfectly - it simply was not applied.
|
||||
|
||||
*Rule: every payload declares which PC types it is for. An undeclared payload
|
||||
installs nowhere and fails validation, rather than installing everywhere.*
|
||||
|
||||
### Gate at the point of action, not the call site
|
||||
|
||||
`Run-ShopfloorSetup.ps1` gated both the S: mapper and the machine-number prompt
|
||||
on PC type. Both registered anyway on a Display, with no "Skipping" line in the
|
||||
log - something in the finalization phase reaches those registrars past the
|
||||
call-site gate. The fix was to gate inside each registrar.
|
||||
|
||||
*Rule: authority checks belong with the thing being done. A caller-side check is
|
||||
a convenience, never the control.*
|
||||
|
||||
### The image itself carries configuration
|
||||
|
||||
`WJ Shopfloor.lnk`, dated April, sits in the all-users Startup folder of every
|
||||
bay - **inside the WIM**. No script change removes it, and no amount of thinning
|
||||
the pipeline touches it.
|
||||
|
||||
*Rule: "thin image" must include auditing what is baked into the WIM, not just
|
||||
what the pipeline adds afterwards.*
|
||||
|
||||
### The provisioning package is a hard boundary
|
||||
|
||||
These are installed by the ppkg, not by anything PXE controls:
|
||||
|
||||
```
|
||||
Google Chrome (+2 updater tasks) RealVNC Tanium
|
||||
CyberArk EPM .NET 3.5 Report IP Sysinternals Autologon DSC
|
||||
PowerShell 7 x64 AND x86
|
||||
```
|
||||
|
||||
*Rule: thinning the image does not thin the package. Anything that must not be
|
||||
on a kiosk and comes from the ppkg needs a package change or an explicit
|
||||
post-install removal - it cannot be solved by scoping our own payloads.*
|
||||
|
||||
---
|
||||
|
||||
## 10. Phased delivery
|
||||
|
||||
Each phase is useful standing alone. No phase requires the next.
|
||||
|
||||
**Phase 0 - settle the unknowns (hours)**
|
||||
- Confirm the LTSC autostart gate first-hand (one bay, hands off, 40 seconds)
|
||||
and record it - expected to wait, per operational experience
|
||||
- Decide: re-implement the imaging step, or accept one click per build
|
||||
- Decide: one image or three
|
||||
- Decide: join at imaging time, at first power-on, or re-inject
|
||||
|
||||
**Phase 1 - visibility (small)**
|
||||
- WinPE reports serial / MAC / model on boot; "awaiting assignment" list in the webapp
|
||||
- Relay imaging status to ShopDB over the GE side
|
||||
- *Useful immediately: you can see what is booting and what it did.*
|
||||
|
||||
**Phase 2 - dual-home safely (small, gating)**
|
||||
- Interface-scope UFW, bind Samba, bring up `enp0s31f6`
|
||||
- Nothing else proceeds safely until this is done
|
||||
|
||||
**Phase 3 - job queue**
|
||||
- Job model + API in ShopDB (create / claim / complete), with the on-screen
|
||||
confirmation code
|
||||
- WinPE polls once where the menu is today; falls back to the menu if no job
|
||||
|
||||
**Phase 4 - first-boot selection**
|
||||
- Selection UI, server-backed lists, POST the choice, re-runnable
|
||||
- Retire the WinPE sub-menus for subtype and tool association
|
||||
|
||||
**Phase 5 - per-machine records**
|
||||
- Import the CSVs and `.reg` files into ShopDB with payload refs
|
||||
- Retire `bay-config.csv`, `cmm-bay-config.csv`, and staging all 147 `.reg` files
|
||||
to every bay
|
||||
|
||||
**Phase 6 - thin the image**
|
||||
- Remove per-type media (`_media/<pctype>`), the staging block, the Office ppkg
|
||||
variants, the CMM bay picker, the WaxTrace ISO cherry-pick
|
||||
- WinPE's job becomes: partition, apply, join, install enforce client, reboot
|
||||
|
||||
---
|
||||
|
||||
## 11. Risks
|
||||
|
||||
| Risk | Why it matters | Mitigation |
|
||||
|---|---|---|
|
||||
| Remote-triggered wipe hits the wrong machine | destructive, irreversible | on-screen confirmation code; job may only target a waiting machine; log against operator and asset |
|
||||
| ShopDB becomes a single point of failure | today a bay configures from the share with ShopDB down | GE-Enforce ETag / last-known-good cache; explicit failure UI, never a silent stall |
|
||||
| Dual-homing exposes the PXE server | SSH `pxe/pxe`, SMB, TFTP currently `ALLOW IN Anywhere` | Phase 2 gates everything |
|
||||
| BPRT expiry on shelf stock | silent, indistinguishable from "no network yet" | pick a join strategy (§8.2) and add an explicit expiry check |
|
||||
| Selection UI becomes the new single point of correctness | wrong subtype = wrong PC-DMIS, wrong bay config | re-runnable, server-corrected, audited |
|
||||
| Re-implementing PESetup | ongoing MCL format tracking + a likely compliance conversation | scope it against "one click per build" - zero-touch is the only thing that needs it |
|
||||
| Big payloads over the corporate network | PC-DMIS, FormTracePak ISOs (~2 GB each), Keyence | GE-Enforce supports `smb` / `http` / `inline` per entry - decide per payload |
|
||||
| Three images drift apart | drift caused the 2026-08-06 outage | prefer one image; `share-drift.py` guards what remains |
|
||||
| API unreachable at first boot | ShopDB needs AESFMA; the imaging LAN cannot reach it | wait-for-network task pattern (§4.2); never assume connectivity |
|
||||
| ppkg-delivered apps on a kiosk | Chrome, RealVNC, Tanium etc. are outside PXE control | package change or explicit post-install removal - scoping our payloads cannot fix it |
|
||||
|
||||
---
|
||||
|
||||
## 12. Open questions
|
||||
|
||||
1. One image or three? What genuinely differs between them?
|
||||
2. Join at imaging time, at first power-on, or re-inject on demand?
|
||||
3. Is the LTSC confirmation truly unskippable? (expected yes - it decides
|
||||
whether zero-touch requires re-implementing the imaging step)
|
||||
4. Is replacing the GE-mandated imaging tool acceptable, if it comes to that?
|
||||
5. Does the selection UI run as SYSTEM (no LAPS password), or as an admin user?
|
||||
6. Which payloads stay on SMB and which move to HTTPS?
|
||||
7. Who operates the job queue day to day - is "Create this PC" a technician
|
||||
action, or does it need approval?
|
||||
|
||||
---
|
||||
|
||||
## 13. Where everything referenced here lives
|
||||
|
||||
### 13.1 This repo (`/home/camp/projects/pxe`)
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `docs/PESETUP-INTERNALS.md` | decompiled imaging tool behaviour; the source for every PESetup claim above |
|
||||
| `docs/OWNERSHIP.md` | who owns what, with the 2026-08-06 collisions |
|
||||
| `playbook/startnet.cmd` | the WinPE script this proposal thins out - menus, staging, verify, log harvest |
|
||||
| `playbook/scripts/preflight.ps1` | bay-side checks (secure boot, disk, driver match, media age) |
|
||||
| `playbook/shopfloor-setup/run-enrollment.ps1` | applies the SFLD package via `provtool` |
|
||||
| `playbook/shopfloor-setup/Run-ShopfloorSetup.ps1` | post-boot setup; the enrollment-model comment is here |
|
||||
| `playbook/shopfloor-setup/Fetch-StagingPayload.ps1`, `Verify-And-Heal-Staging.ps1` | first-logon self-heal; read `C:\Enrollment\fetch-source.txt` |
|
||||
| `playbook/shopfloor-setup/BPRT/Select-PCConfig.ps1` | the pre-seed shim (vendor GUI preserved as `-vendor.ps1` on the share) |
|
||||
| `playbook/shopfloor-setup/BIOS/{check-bios.cmd,models.txt}` | firmware matching |
|
||||
| `scripts/preflight.py` | runs every server-side lint in one command |
|
||||
| `scripts/lint-unattend.py` | schema limits: Path 259, CommandLine 1024, Description 256 |
|
||||
| `scripts/lint-driver-catalogue.py` | reimplements `GetDriverByModel` |
|
||||
| `scripts/share-drift.py` | repo vs live share, git-owned vs unreconciled |
|
||||
| `playbook/shopfloor-setup/Collect-ImagingDiagnostics.ps1` | one-pass bay diagnostics - identity, enrollment, installed apps, all four autostart surfaces, GE-Enforce reachability |
|
||||
| `scripts/build-pctype-media.py` | per-PCTYPE media views (retired by Phase 6) |
|
||||
|
||||
### 13.2 PXE server (`pxe@172.16.9.1`)
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `/srv/samba/winpeapps/<image>/Deploy/Control/` | `HardwareDriver.json`, `OperatingSystem.json`, `Media.tag`, `Release.json` |
|
||||
| `/srv/samba/winpeapps/<image>/Deploy/FlatUnattendW10.xml` | the answer file that actually boots machines |
|
||||
| `/srv/samba/winpeapps/_shared/Applications/BPRT/` | the six MCL packages, `Insert-BPRTToPPKG.ps1`, `Start-BulkEnrollOrchestrator.ps1`, `Select-PCConfig*.ps1` |
|
||||
| `/srv/samba/winpeapps/_shared/BIOS/` | firmware EXEs + `models.txt` (served as the `winpeapps_bios` share) |
|
||||
| `/srv/samba/winpeapps/_media/<pctype>/` | per-PCTYPE media views |
|
||||
| `/srv/samba/enrollment/scripts/` | `run-enrollment.ps1`, `preflight.ps1`, `winpe-status-push.ps1` |
|
||||
| `/srv/samba/enrollment/shopfloor-setup/` | per-PC-type trees, `menu.json`, `_ntlars-backups/` (147 `.reg`) |
|
||||
| `/srv/samba/enrollment/shopfloor-setup/gea-shopfloor-waxtrace/bay-config.csv` | 15 bays, FTPak versions |
|
||||
| `/srv/samba/enrollment/installers-post/cmm/cmm-bay-config.csv` | CMM bay -> version, DODA |
|
||||
| `/srv/samba/enrollment/pre-install/udc-backups/` | per-bay UDC settings |
|
||||
| `/srv/samba/enrollment/ppkgs/` | the SFLD package (8.2 GB) |
|
||||
| `/srv/samba/enrollment/imaging-logs/<serial>/` | harvested imaging logs (added 2026-08-06) |
|
||||
| `/var/www/html/win11/sources/boot.wim` | the booted WinPE image |
|
||||
| `/var/log/pxe-imaging/<serial>.json` | per-bay stage history |
|
||||
| `/opt/pxe-webapp/` | Flask webapp (unit `pxe-webapp`, :9009) |
|
||||
| `/etc/dnsmasq.conf`, `/etc/samba/smb.conf` | the binding config §6.2 depends on |
|
||||
|
||||
### 13.3 shopdb-flask (`/home/camp/projects/shopdb-flask`)
|
||||
|
||||
| Path | What |
|
||||
|---|---|
|
||||
| `plugins/geenforce/seed_display_scope.py` | the pattern this proposal generalises - subtype map, server role by IP, sweep |
|
||||
| `plugins/geenforce/client/` | `ShopdbEnforceClient.psm1`, `Invoke-ShopdbEnforce.ps1` |
|
||||
| `shopdb/core/api/` | collector endpoint, dashboard-defaults display-role |
|
||||
|
||||
### 13.4 Evidence behind the claims in this document
|
||||
|
||||
| Path | What it shows |
|
||||
|---|---|
|
||||
| `/home/camp/pxe-images/now/pesetuplog.txt` | PESetup 4.0.0.20 run - media drive, disking, copy, driver selection, the `Btn 'Next'` timing in §7.1 |
|
||||
| `/home/camp/pxe-images/now/debug.txt` | the WinPE console for the failed volume-finder run |
|
||||
| `/home/camp/pxe-images/pxe2/Panther/` | `setupact.log` / `setuperr.log` naming the unattend `Path` failure |
|
||||
| `/home/camp/pxe-images/debug2/` | the autologon registry state and the sysprep-loop timeline |
|
||||
| `/home/camp/pxe-images/debug3/Logs/BPRT/` | the 13 package components, `criticalChecks.json`, `packageInfo.json` |
|
||||
| `/home/camp/pxe-images/debug3/Logs/PPKG/` | the package itself, `provisioning-sessions.json`, the diagnostics evtx |
|
||||
| `/home/camp/pxe-images/debug3/Logs/enrollment.log` | the double run and the rename race in §9 |
|
||||
| `/home/camp/pxe-images/debug3/Logs/FilteredReportIP.log` | proves the bay had no route to Entra (§8.2) |
|
||||
|
||||
### 13.5 On-bay locations (for anyone diagnosing one)
|
||||
|
||||
```
|
||||
C:\Enrollment\ staged payload, pc-type.txt, display-type.txt,
|
||||
pc-config.txt, fetch-source.txt, winpe-staging.log
|
||||
C:\Deploy\Applications\BPRT\ the packages + orchestrator, copied by PESetup
|
||||
C:\Logs\BPRT\<Component>\ per-component package logs (UTF-16)
|
||||
C:\Logs\BPRT\criticalChecks.json PPKG matched / Entra joined / OOBE execution
|
||||
C:\Logs\PPKG\ provisioning diagnostics + session state
|
||||
C:\Logs\SFLD\ DSC deployment, sync_intune, shopfloor-setup
|
||||
C:\Logs\enrollment.log run-enrollment.ps1
|
||||
C:\Windows\Panther\ setupact.log, setuperr.log, unattend.xml, PESetup*.log
|
||||
C:\Windows\Panther\UnattendGC\ oobeSystem pass
|
||||
```
|
||||
|
||||
### 13.5a Gotchas any new tooling will hit
|
||||
|
||||
- **`HardwareDriver.json` cannot be read by `ConvertFrom-Json`.** It carries both
|
||||
casings of the same fields (`fileName`/`FileName`, `destinationDir`/
|
||||
`DestinationDir`) and PowerShell rejects the document - on 5.1 *and* 7.
|
||||
`-AsHashtable` is PS6+ and WinPE runs 5.1. Extract fields by regex, or parse in
|
||||
Python where duplicate keys are tolerated.
|
||||
- **`Compress-Archive` caps at 2 GB**, and `run-enrollment` harvests the 8 GB
|
||||
provisioning package into `C:\Logs\PPKG`, so any log collection must exclude
|
||||
`*.ppkg`.
|
||||
- **Backslash-vs-forward-slash**: PowerShell Core normalises `\` to `/` on Linux,
|
||||
so `Join-Path` with Windows separators works cross-platform - handy for testing
|
||||
bay scripts on the dev box.
|
||||
|
||||
### 13.6 How to re-derive things
|
||||
|
||||
```bash
|
||||
# read a file off the share
|
||||
sshpass -p pxe ssh -o StrictHostKeyChecking=no pxe@172.16.9.1 'cat <path>'
|
||||
smbclient //172.16.9.1/enrollment -U pxe-upload%pxe -c 'ls'
|
||||
|
||||
# what is actually inside the booted WinPE
|
||||
sudo wimextract /var/www/html/win11/sources/boot.wim 1 \
|
||||
/Windows/System32/startnet.cmd --dest-dir=/tmp/x
|
||||
sudo wimdir /var/www/html/win11/sources/boot.wim 1 | grep -i <name>
|
||||
|
||||
# a .ppkg is a WIM - pull out the spec without extracting 8 GB
|
||||
7z l <pkg>.ppkg
|
||||
7z e <pkg>.ppkg -o<dir> "Multivariant/0/customizations.xml" \
|
||||
"Multivariant/0/Prov/RunTime/0__Accounts_Azure.provxml"
|
||||
|
||||
# provisioning diagnostics (python-evtx is installed)
|
||||
python3 -c "from Evtx.Evtx import Evtx;
|
||||
[print(r.xml()) for r in Evtx('Provisioning-Diagnostics-Admin.evtx').records()]"
|
||||
|
||||
# decompile PESetup - the bundle-extraction recipe is in PESETUP-INTERNALS.md
|
||||
ilspycmd -t <TypeName> PESetup.dll
|
||||
|
||||
# validate before deploying
|
||||
./scripts/preflight.py # every server-side lint
|
||||
./scripts/share-drift.py --diff # repo vs live
|
||||
```
|
||||
|
||||
### 13.7 The 2026-08-06 commits this document draws on
|
||||
|
||||
```
|
||||
dfa026b lint-driver-catalogue c7b1769 build-pctype-media
|
||||
049dd7b startnet per-PCTYPE media d5f7abc BIOS 7020 family
|
||||
385acfe lint-unattend 2b6189f startnet W: wait + honest reporting
|
||||
d04865d PESetup version + LogonCount b96ff7e retire shopfloor-dashboard route
|
||||
36be60e run-enrollment stops renaming 8c21282 staging verify + log harvest
|
||||
d2200e8 preflight (bay + server) 68df59e OWNERSHIP.md + share-drift
|
||||
9d51c0b purpose menu + 3D kiosk a0aceb4 PXESERVER consolidation
|
||||
bb08392 download-drivers case fix
|
||||
```
|
||||
1071
docs/kb/How_To_Deploy_PXE_Server_New_Site.html
Normal file
1071
docs/kb/How_To_Deploy_PXE_Server_New_Site.html
Normal file
File diff suppressed because one or more lines are too long
296
docs/mtconnect-followup-runbook.md
Normal file
296
docs/mtconnect-followup-runbook.md
Normal file
@@ -0,0 +1,296 @@
|
||||
# MTConnect follow-up runbook
|
||||
|
||||
Written 2026-08-11. Everything here needs a machine this box cannot reach - the
|
||||
SFLD share, the middle-man PC, or a shopfloor bay - so it is yours to run.
|
||||
|
||||
Work in the order given. Part 1 changes what future installs get; parts 2 and 3
|
||||
fix bays that are already wrong; parts 4 and 5 are questions to answer before
|
||||
any more code is written.
|
||||
|
||||
### Identify every bay by machine number
|
||||
|
||||
The machine number is the only identifier that stays put. A PC gets replaced or
|
||||
re-imaged and its name changes; the machine it sits at does not. Both machine-to-PC
|
||||
sources used to build this document are point-in-time snapshots and will already
|
||||
have drifted.
|
||||
|
||||
Every PC name below came from `mtconnect-hostname-machine-map.csv` (12 June),
|
||||
the `Shop Floor PC Name` column of `West Jefferson_Machines.csv`, or the variant
|
||||
captures. Treat them as "this is where the fault was last seen" - useful for
|
||||
finding the bay, never for deciding what to write into it.
|
||||
|
||||
**The bay knows its own number.** In order of authority:
|
||||
|
||||
```powershell
|
||||
# 1. eDNC registry - what GE-Enforce trusts post-imaging
|
||||
(Get-ItemProperty 'HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\General' -Name MachineNo).MachineNo
|
||||
# 2. the enrollment file imaging writes
|
||||
Get-Content C:\Enrollment\machine-number.txt
|
||||
# 3. UDC
|
||||
(Get-Content C:\ProgramData\UDC\udc_settings.json -Raw | ConvertFrom-Json).GeneralSettings.MachineNumber
|
||||
```
|
||||
|
||||
Or dot-source the lib and ask for both at once:
|
||||
|
||||
```powershell
|
||||
. C:\GE Aerospace\lib\Update-MachineNumber.ps1
|
||||
Get-CurrentMachineNumber # @{ Udc = ...; Ednc = ... }
|
||||
```
|
||||
|
||||
**ShopDB is the live PC-to-machine record**, maintained by the collector rather
|
||||
than by hand, so it beats any CSV:
|
||||
|
||||
```
|
||||
https://tsgwp00525.wjs.geaerospace.net/shopdb/api/computers/by-hostname/<HOSTNAME>
|
||||
```
|
||||
|
||||
If those disagree with each other, or with the machine plate, stop and resolve
|
||||
that first - a rename applied to the wrong number is worse than the mismatch
|
||||
you started with.
|
||||
|
||||
---
|
||||
|
||||
## Part 1. Push the Fanuc adapter 3.25.3 to the SFLD share
|
||||
|
||||
The staged bundle here is already on 3.25.3.0 (`sha256` starts `6e46dcd51affa9e0`).
|
||||
The live share is not. Until this is copied, new installs still get 3.23.0.0.
|
||||
|
||||
**1.1** From the middle-man PC, with `Z:` on the dev share:
|
||||
|
||||
```
|
||||
Z:\mtconnect-bundles\fanuc\Adapter\
|
||||
```
|
||||
|
||||
should contain `FanucCppAdapterWinX64.exe` at **3,369,984 bytes**. Confirm before copying:
|
||||
|
||||
```powershell
|
||||
(Get-Item Z:\mtconnect-bundles\fanuc\Adapter\FanucCppAdapterWinX64.exe).Length
|
||||
(Get-FileHash Z:\mtconnect-bundles\fanuc\Adapter\FanucCppAdapterWinX64.exe -Algorithm SHA256).Hash.Substring(0,16)
|
||||
# expect 3369984 and 6E46DCD51AFFA9E0
|
||||
```
|
||||
|
||||
**1.2** Back up what is on the share now, then copy the three files:
|
||||
|
||||
```powershell
|
||||
$share = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\main\machineapps\mtconnect-bundles\fanuc\Adapter'
|
||||
Copy-Item $share\FanucCppAdapterWinX64.exe "$share\..\FanucCppAdapterWinX64-3.23.0-replaced-$(Get-Date -f yyyyMMdd).exe"
|
||||
Copy-Item Z:\mtconnect-bundles\fanuc\Adapter\FanucCppAdapterWinX64.exe $share -Force
|
||||
Copy-Item Z:\mtconnect-bundles\fanuc\Adapter\Fwlib64.dll $share -Force
|
||||
Copy-Item Z:\mtconnect-bundles\fanuc\Adapter\fwlibe64.dll $share -Force
|
||||
```
|
||||
|
||||
**1.3** Verify on the share:
|
||||
|
||||
```powershell
|
||||
(Get-FileHash $share\FanucCppAdapterWinX64.exe -Algorithm SHA256).Hash.Substring(0,16) # 6E46DCD51AFFA9E0
|
||||
```
|
||||
|
||||
`adapter.xml` is deliberately NOT copied. The config vocabulary is identical
|
||||
between 3.23 and 3.25.3, and the share's copy carries site settings.
|
||||
|
||||
**Rollback:** copy the `-3.23.0-replaced-` file back over `FanucCppAdapterWinX64.exe`.
|
||||
|
||||
---
|
||||
|
||||
## Part 2. Fix the two Makino bays
|
||||
|
||||
Both have the agent on the machine number and the other three files still on
|
||||
`MAKINO-1`. The fixed `Update-MachineNumber.ps1` converges them.
|
||||
|
||||
| machine | PC when captured | confirm at the bay |
|
||||
|---|---|---|
|
||||
| **7502** | `G5W5V7V3ESF` | eDNC reg / enrollment file / plate |
|
||||
| **7506** | `G4H8KF33ESF` | eDNC reg / enrollment file / plate |
|
||||
|
||||
Find the current PC for a machine number in ShopDB rather than trusting the
|
||||
name above; if the PC was replaced, the new one may never have had the bad
|
||||
rename applied, in which case check its four files before doing anything.
|
||||
|
||||
**2.1** On the bay, elevated PowerShell:
|
||||
|
||||
```powershell
|
||||
. C:\GE Aerospace\lib\Update-MachineNumber.ps1 # or wherever the lib landed
|
||||
$r = Update-MTConnectVariantName -NewNumber '7502' `
|
||||
-Root 'C:\Makino-MTConnect' `
|
||||
-Targets @(
|
||||
@{ Path='Agent\Devices.xml'; Kind='attr' },
|
||||
@{ Path='Agent\Devices.EDM.xml'; Kind='attr' },
|
||||
@{ Path='Data\AdapterDataFile.xml'; Kind='name' },
|
||||
@{ Path='Agent\Agent.cfg'; Kind='cfg' }
|
||||
) `
|
||||
-Services @('Makino Adapter Manager','Makino MTConnect Agent')
|
||||
$r.Updated; $r.Errors
|
||||
```
|
||||
|
||||
Use the bay's own number - do not paste 7502 onto 7506.
|
||||
|
||||
**2.2** Verify all four agree, and nothing still says MAKINO:
|
||||
|
||||
```powershell
|
||||
Select-String -Path C:\Makino-MTConnect\Agent\Devices.xml,`
|
||||
C:\Makino-MTConnect\Agent\Devices.EDM.xml,`
|
||||
C:\Makino-MTConnect\Data\AdapterDataFile.xml,`
|
||||
C:\Makino-MTConnect\Agent\Agent.cfg `
|
||||
-Pattern 'MAKINO-\d+|7502'
|
||||
```
|
||||
|
||||
**2.3** Confirm it is serving:
|
||||
|
||||
```powershell
|
||||
(Invoke-WebRequest http://localhost:5000/current -UseBasicParsing).Content -match '<MTConnectDevices'
|
||||
```
|
||||
|
||||
**Watch for:** the vendor guide says the Adapter Manager rewrites its whole
|
||||
configuration from memory when it stops. The function stops it first, but this
|
||||
has never been proven on real hardware. If the names revert after a reboot,
|
||||
stop here and tell me - that changes the approach.
|
||||
|
||||
---
|
||||
|
||||
## Part 3. Fix the seven Fanuc bays
|
||||
|
||||
The agent and the adapter disagree. Three have a correct agent and a stale
|
||||
adapter; four have no agent device name at all and need looking at before any
|
||||
rename (Part 4).
|
||||
|
||||
**Rename these three.** The "correct number" column is what the June map said -
|
||||
it is a starting point, not authority. Confirm from the bay itself (see the
|
||||
note at the top) before running anything:
|
||||
|
||||
| machine (per map) | adapter says | agent says | PC when captured |
|
||||
|---|---|---|---|
|
||||
| **7801** | 4007 | 7801 | `G9N2JNZ3ESF` |
|
||||
| **7804** | 3031 | 7804 | `GG1J98Y3ESF` |
|
||||
| **2005 or 2006** | 2005 | 2006 | `GC7ZN7V3ESF` |
|
||||
|
||||
`GC7ZN7V3ESF` is the one to be careful with: 2005 and 2006 are a Hwacheon
|
||||
dual-spindle pair sharing a PC, so two machines legitimately answer to one
|
||||
hostname and the map and the agent disagree about which. Read the machine plate
|
||||
and the eDNC registry before running it.
|
||||
|
||||
**3.1** On the bay, elevated:
|
||||
|
||||
```powershell
|
||||
. C:\GE Aerospace\lib\Update-MachineNumber.ps1
|
||||
$r = Update-MTConnectVariantName -NewNumber '7801' `
|
||||
-Root 'C:\MTConnect' `
|
||||
-Targets @(
|
||||
@{ Path='Agent\devices.xml'; Kind='attr' },
|
||||
@{ Path='Adapter\devices.xml'; Kind='attr' },
|
||||
@{ Path='Adapter\adapter.xml'; Kind='elem' }
|
||||
) `
|
||||
-Services @('MTConnect Adapter Fanuc','MTConnect Agent Fanuc')
|
||||
$r.Updated; $r.Errors
|
||||
```
|
||||
|
||||
**3.2** Verify the three files agree:
|
||||
|
||||
```powershell
|
||||
Select-String -Path C:\MTConnect\Agent\devices.xml,C:\MTConnect\Adapter\devices.xml `
|
||||
-Pattern 'name="[^"]+"' | Select-Object -First 2
|
||||
Select-String -Path C:\MTConnect\Adapter\adapter.xml -Pattern '<Device(Name|ID|UUID)>[^<]+'
|
||||
```
|
||||
|
||||
All should read the same number. Then the HTTP probe as in 2.3 (port 5000).
|
||||
|
||||
---
|
||||
|
||||
## Part 4. Four Fanuc bays with no agent device name
|
||||
|
||||
| machine (per map) | adapter says | PC when captured |
|
||||
|---|---|---|
|
||||
| **2013** | 4007 | `GJBJC724ESF` |
|
||||
| **7606** | 3126 | `G7QLY5X3ESF` |
|
||||
| **4002** | 4002 | `G6JLMSZ2ESF` |
|
||||
| **2001** | 3115 | `GB07T5X3ESF` |
|
||||
|
||||
These are not a rename problem - the agent's `devices.xml` has no `<Device
|
||||
name=...>` at all, so the agent has nothing to serve. Do NOT run Part 3 on them.
|
||||
|
||||
On each, collect and send back:
|
||||
|
||||
```powershell
|
||||
Get-Service 'MTConnect*' | Select-Object Name,Status,StartType
|
||||
Get-Content C:\MTConnect\Agent\devices.xml -TotalCount 20
|
||||
Test-Path C:\MTConnect\Agent\devices.xml
|
||||
(Invoke-WebRequest http://localhost:5000/current -UseBasicParsing -TimeoutSec 5).StatusCode
|
||||
Get-Content C:\MTConnect\Agent\agent.log -Tail 30
|
||||
```
|
||||
|
||||
Note three of the four also disagree with the hostname map on the number
|
||||
(4007 vs 2013, 3126 vs 7606, 3115 vs 2001), so these bays may have been
|
||||
re-purposed without a full reconfigure.
|
||||
|
||||
---
|
||||
|
||||
## Part 5. Two questions to settle
|
||||
|
||||
**5.1 DMG Mori - do they need a bundle at all?**
|
||||
|
||||
Five machines, all flagged `1,2,3` in the machine list, no bundle among the
|
||||
five we ship, and no captured variant. Their controllers are `DMG Mori F31i
|
||||
A5/B5` - Fanuc 31i underneath - so the Fanuc adapter may already serve them.
|
||||
|
||||
| machine | PC per the machine list |
|
||||
|---|---|
|
||||
| WJ-7401 | `G1KDN7X2ESF` |
|
||||
| WJ-7402 | `G1KCR7X2ESF` |
|
||||
| WJ-7403 | `G1HYK7X2ESF` |
|
||||
| WJ-7404 | `G1HGQ7X2ESF` |
|
||||
| WJ-7405 | `G1KLM7X2ESF` |
|
||||
|
||||
Those names come from the `Shop Floor PC Name` column of the OneDrive machine
|
||||
list and are subject to the same staleness - look the machine up in ShopDB for
|
||||
the PC that is actually on it today.
|
||||
|
||||
On any one of them:
|
||||
|
||||
```powershell
|
||||
Get-Service | Where-Object { $_.Name -match 'MTConnect|Makino|Coordinator' } |
|
||||
Select-Object Name,Status,StartType
|
||||
Get-ChildItem C:\ -Directory | Where-Object Name -match 'MTConnect|DMG|Coordinator'
|
||||
(Invoke-WebRequest http://localhost:5000/current -UseBasicParsing -TimeoutSec 5).StatusCode
|
||||
```
|
||||
|
||||
If they run the Fanuc bundle already, no new variant is needed and the DMG
|
||||
Coordinator MSI in the OneDrive zip can be ignored. If they collect some other
|
||||
way, or not at all, that is the answer to whether a sixth bundle is worth
|
||||
building.
|
||||
|
||||
**5.2 Does the live v2 manifest carry the MTConnect entries?**
|
||||
|
||||
You are on v2. The concern is that the MTConnect entries were left behind in
|
||||
the dead v1 manifest, in which case v2 bays are not getting MTConnect from the
|
||||
manifest engine at all.
|
||||
|
||||
```powershell
|
||||
$m = Get-Content '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\main\machineapps\machineapps-manifest.json' -Raw | ConvertFrom-Json
|
||||
$m.Applications.Count
|
||||
$m.Applications | ForEach-Object { $_.Name }
|
||||
```
|
||||
|
||||
Expect 8 applications, five of them MTConnect (fanuc, okuma, edncStandard,
|
||||
edncDualSpindle, makino). Anything less and the missing ones never made it to
|
||||
v2.
|
||||
|
||||
---
|
||||
|
||||
## A note on the CSVs
|
||||
|
||||
Neither machine-to-PC list will age well: `mtconnect-hostname-machine-map.csv`
|
||||
(12 June) and the `Shop Floor PC Name` column in `West Jefferson_Machines.csv`
|
||||
are both hand-built snapshots, and every PC swap silently invalidates a row.
|
||||
|
||||
ShopDB now maintains the PC-to-machine link from collector reports, so it
|
||||
corrects itself as bays are re-imaged. Anything that needs to go from a machine
|
||||
number to "which PC is on it today" should ask ShopDB; if a CSV is still wanted
|
||||
for offline use, generate it from ShopDB rather than editing it by hand.
|
||||
|
||||
## What I still owe you
|
||||
|
||||
- Auto-detection of controller and variant for a new machine. The inputs exist
|
||||
(`mtconnect-bay-config.csv` has controller IP and FOCAS port per machine, the
|
||||
machine list has the controller model, the collector already knows the machine
|
||||
number at imaging time) - design pending.
|
||||
- The code fix is committed and pushed as `0d9c4fd` on `pxe-webapp-redesign`.
|
||||
It changes future renames only; it does not reach out and fix these bays.
|
||||
203
docs/pxe-nic-guard.md
Normal file
203
docs/pxe-nic-guard.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# PXE NIC guard
|
||||
|
||||
The PXE server's DHCP/PXE (dnsmasq) must never serve on the GE corporate
|
||||
network. Each server has two NICs:
|
||||
|
||||
| Role | Network | Address |
|
||||
|---------|-----------------------------|----------------------------------|
|
||||
| imaging | isolated imaging switch | static 172.16.9.1/24, dnsmasq |
|
||||
| ge | GE corporate network | DHCP client (systemd-networkd) |
|
||||
|
||||
`/etc/dnsmasq.d/pxe-nic-only.conf` pins dnsmasq to the imaging NIC by name.
|
||||
That stops dnsmasq answering on the GE NIC, but not on the imaging NIC when
|
||||
that NIC itself ends up on the wrong network: a cable swap, an imaging
|
||||
switch uplinked into a GE port, or a second PXE server on the same switch.
|
||||
The NIC guard catches those.
|
||||
|
||||
## What is installed
|
||||
|
||||
| File | Purpose |
|
||||
|-----------------------------------------------------------|---------------------------------|
|
||||
| `/usr/local/sbin/pxe-nic-check` | the classifier |
|
||||
| `/etc/pxe-server/nic.conf` | which NIC is which |
|
||||
| `/etc/systemd/system/dnsmasq.service.d/nic-guard.conf` | guard before every dnsmasq start|
|
||||
| `/etc/systemd/system/pxe-nic-check.{service,timer}` | watch every 5 min |
|
||||
| `/usr/local/sbin/pxe-nic-carrier` | re-check on imaging cable moves |
|
||||
| `/etc/networkd-dispatcher/<state>.d/50-pxe-nic-carrier` | calls it on link changes |
|
||||
| `/etc/systemd/system/pxe-nic-carrier.service` | the re-check after carrier gain |
|
||||
| `/run/pxe-nic-status.json` | last result, read by the webapp |
|
||||
|
||||
Sources: `playbook/pxe-server-helpers/`, installed by `pxe_server_setup.yml`
|
||||
(vars `pxe_nic_mac`, `corp_nic_mac`, `pxe_corp_domain_suffixes`,
|
||||
`pxe_nic_allowed_switches`; `pxe_imaging_mac` / `pxe_ge_mac` are derived
|
||||
from the first two). The same MACs pin the NICs in netplan
|
||||
(`playbook/templates/netplan-pxe.yaml.j2`, names `pxe0` / `corp0`) and in
|
||||
`/etc/dnsmasq.d/pxe-nic-only.conf`. With `pxe_nic_mac` empty the playbook
|
||||
picks the imaging NIC at install time with `pxe-pick-nic --auto`, which
|
||||
runs this check on the one port with a link (`--iface X --probe`, 65 s)
|
||||
and accepts only "isolated". With `corp_nic_mac` empty `ge_mac` is blank
|
||||
and the GE NIC is whichever other port holds a DHCP lease.
|
||||
|
||||
## How it decides
|
||||
|
||||
It listens on each NIC (raw socket, sends nothing; it joins the STP,
|
||||
802.1X, LLDP, CDP and PVST+ multicast groups so a NIC that is not
|
||||
promiscuous still hears them) for:
|
||||
|
||||
- LLDP and CDP: managed switches announce themselves. GE switches do; the
|
||||
unmanaged imaging switch does not.
|
||||
- 802.1X EAPOL from an authenticator: GE ports ask for port
|
||||
authentication (EAP Request, Success or Failure). EAPOL that clients
|
||||
send is ignored: iPXE sends three EAPOL-Starts every time it opens a
|
||||
NIC and Windows sends Starts and EAP Responses, so a bay PXE-booting on
|
||||
the imaging switch is not mistaken for a GE port. Those frames show up
|
||||
only as "N client EAPOL frames ignored".
|
||||
- DHCP OFFER/ACK from any other server.
|
||||
- STP and the count of distinct ARP hosts: reported, never decisive.
|
||||
|
||||
On the imaging NIC it also sends one DHCPDISCOVER and collects offers for
|
||||
6 s. On the GE NIC it reads the networkd lease instead (no traffic).
|
||||
|
||||
| Verdict | Meaning |
|
||||
|----------------|------------------------------------------------------------|
|
||||
| `isolated` | no foreign DHCP, no LLDP/CDP/EAPOL |
|
||||
| `ge` | LLDP, CDP or EAPOL seen, or a DHCP server whose domain ends in a corp suffix (`geaerospace.net`, `ge.com`; whole labels, so `notgeaerospace.net` does not count) |
|
||||
| `foreign-dhcp` | another DHCP server, no corp domain |
|
||||
| `no-link` | no carrier |
|
||||
| `error` | the check itself failed |
|
||||
|
||||
Healthy: imaging NIC `isolated` (or `no-link`), GE NIC `ge`.
|
||||
|
||||
Our own dnsmasq never counts. An offer whose server id is our own address
|
||||
but comes from a MAC that is not ours is a second PXE server and DOES count.
|
||||
|
||||
If the imaging switch is itself a managed switch that sends LLDP/CDP, put
|
||||
its LLDP system name, CDP device id or chassis MAC in `allowed_switches`
|
||||
(`pxe_nic_allowed_switches`), or the guard will block dnsmasq. 802.1X is
|
||||
never allowed.
|
||||
|
||||
## Modes
|
||||
|
||||
- `pxe-nic-check` - check every NIC (60 s), print a summary. `--json`,
|
||||
`--write`, `--probe` (imaging DHCP probe), `--iface NAME`, `--window S`.
|
||||
- `pxe-nic-check --guard` - dnsmasq `ExecStartPre`. Imaging NIC only,
|
||||
about 6 s. Exit 1 = do not serve: dnsmasq does not start. A bug or no
|
||||
link exits 0 (fail open) with an error line in the journal.
|
||||
- `pxe-nic-check --watch` - the timer. On a bad imaging verdict it runs
|
||||
`systemctl stop dnsmasq` and logs CRIT. It never starts dnsmasq. If
|
||||
dnsmasq was (re)started during the 60 s window (an operator, or the
|
||||
carrier hook, restarted it and its guard passed), the watch re-checks
|
||||
for about 6 s and stops dnsmasq only if that still says blocked; if
|
||||
dnsmasq is already stopped it only logs.
|
||||
- `pxe-nic-carrier` - the networkd-dispatcher hook. Only the imaging NIC
|
||||
(or its bridge ports) counts; GE NIC events are ignored. Carrier lost:
|
||||
dnsmasq is stopped at once if it was running. Carrier back: after 2 s,
|
||||
`pxe-nic-carrier.service` restarts dnsmasq if it was running or the hook
|
||||
stopped it, so the guard checks the new network before dnsmasq serves
|
||||
again; dnsmasq that an operator or the watch stopped stays stopped.
|
||||
A bridge with several ports stops dnsmasq when any one of them loses
|
||||
carrier. Log: `journalctl -t pxe-nic-carrier`.
|
||||
|
||||
Every start or restart of dnsmasq runs the guard, so `systemctl restart
|
||||
dnsmasq` and the `@reboot` cron restart take about 6 s longer than before.
|
||||
|
||||
## Reading it
|
||||
|
||||
sudo pxe-nic-check # full summary, 60 s
|
||||
cat /run/pxe-nic-status.json # last guard/watch result
|
||||
journalctl -u dnsmasq -b | grep pxe-nic-check
|
||||
journalctl -u pxe-nic-check -b
|
||||
journalctl -t pxe-nic-carrier -b # cable unplug / replug
|
||||
|
||||
## dnsmasq will not start
|
||||
|
||||
1. `journalctl -u dnsmasq -b | grep pxe-nic-check` shows the evidence, e.g.
|
||||
`REFUSING to start dnsmasq: imaging NIC br-pxe verdict ge: LLDP from
|
||||
WJ-IDF2 port Gi1/0/14`.
|
||||
2. Check the imaging cabling: the imaging NIC must go to the imaging
|
||||
switch only, and that switch must not be uplinked anywhere.
|
||||
3. `sudo pxe-nic-check` until the imaging NIC reads `isolated`.
|
||||
4. `sudo systemctl start dnsmasq` (runs the guard again).
|
||||
|
||||
## Floor tests (run with the floor idle)
|
||||
|
||||
All three tests stop imaging for a few minutes. Have console or GE-side SSH
|
||||
access to the server; do not rely on the imaging network for the session.
|
||||
|
||||
### (a) Rogue DHCP on the imaging switch: expect the alarm
|
||||
|
||||
1. Confirm the baseline: `sudo pxe-nic-check --probe` shows the imaging
|
||||
NIC `isolated` and `dnsmasq_bound` true.
|
||||
2. Plug a device that runs a DHCP server into a free port of the IMAGING
|
||||
switch (a spare home router with DHCP on, LAN port only, nothing in its
|
||||
WAN port; or a laptop running dnsmasq).
|
||||
3. Run the watch now instead of waiting up to 5 min:
|
||||
`sudo systemctl start pxe-nic-check.service`
|
||||
4. Expect:
|
||||
- `journalctl -u pxe-nic-check -n 20` has a CRIT line `imaging NIC is
|
||||
NOT isolated, stopping dnsmasq: ... verdict foreign-dhcp: DHCP from
|
||||
192.168.x.1, ...` (or `ge` if the router sends LLDP).
|
||||
- `systemctl is-active dnsmasq` is `inactive`.
|
||||
- `/run/pxe-nic-status.json` has `"dhcp_guard": {"state": "blocked", ...}`.
|
||||
5. Prove the guard too: `sudo systemctl start dnsmasq` must FAIL, and
|
||||
`journalctl -u dnsmasq -n 20` shows `REFUSING to start dnsmasq`.
|
||||
6. Unplug the rogue device. `sudo pxe-nic-check --probe` shows `isolated`.
|
||||
`sudo systemctl start dnsmasq` starts. PXE-boot one client to confirm.
|
||||
|
||||
### (b) Cable swap into a GE port: expect the guard to refuse dnsmasq
|
||||
|
||||
dnsmasq must be stopped BEFORE the cable moves, so it never serves on GE.
|
||||
|
||||
1. `sudo systemctl stop dnsmasq`; `systemctl is-active dnsmasq` must say
|
||||
`inactive`. Do not reboot during the test (the `@reboot` cron restarts
|
||||
dnsmasq; the guard would still block it, but do not test that way).
|
||||
2. Move the IMAGING NIC's cable from the imaging switch into a live GE
|
||||
network port. Wait for link: `ip -br link` shows the imaging NIC `UP`.
|
||||
The carrier hook sees the link come up and runs the guard once (one
|
||||
DHCPDISCOVER) to refresh the status file; with dnsmasq stopped it does
|
||||
not start it. `journalctl -t pxe-nic-carrier -n 5` shows `dnsmasq was
|
||||
not running, left stopped`.
|
||||
3. Run the guard by hand first:
|
||||
`sudo pxe-nic-check --guard; echo "exit $?"`
|
||||
Expect `exit 1` and a `REFUSING to start dnsmasq` line naming LLDP/CDP,
|
||||
802.1X, or `DHCP from 10.x.x.x, domain ....geaerospace.net`.
|
||||
This sends one DHCPDISCOVER on the GE port, the same as any PC being
|
||||
plugged in. To send nothing, use `--no-probe --window 60` instead and
|
||||
rely on LLDP/CDP/802.1X (switches announce every 30-60 s).
|
||||
**If it exits 0, STOP: do not start dnsmasq. Move the cable back and
|
||||
send the output of `sudo pxe-nic-check --probe --json`.**
|
||||
4. Only after step 3 exited 1: `sudo systemctl start dnsmasq`. Expect
|
||||
`Job for dnsmasq.service failed`; `systemctl is-active dnsmasq` says
|
||||
`failed`; `journalctl -u dnsmasq -n 20` shows `REFUSING`.
|
||||
5. Move the cable back to the imaging switch. `sudo pxe-nic-check --probe`
|
||||
shows the imaging NIC `isolated`. `sudo systemctl start dnsmasq`
|
||||
starts. PXE-boot one client to confirm.
|
||||
|
||||
### (c) Imaging cable unplugged and replugged: expect stop, re-check, start
|
||||
|
||||
1. dnsmasq running, imaging NIC `isolated`.
|
||||
2. Unplug the imaging NIC's cable. Within a second
|
||||
`journalctl -t pxe-nic-carrier -n 5` shows `carrier lost on imaging NIC
|
||||
... stopped dnsmasq` and `systemctl is-active dnsmasq` is `inactive`.
|
||||
3. Plug it back into the imaging switch. About 10 s later the journal
|
||||
shows `guard passed, dnsmasq (re)started` and dnsmasq is `active`.
|
||||
Had it gone into a GE port instead, the guard refuses: the journal
|
||||
shows `dnsmasq NOT started` and dnsmasq stays down.
|
||||
|
||||
## Status file
|
||||
|
||||
`/run/pxe-nic-status.json`, mode 0644, replaced atomically:
|
||||
|
||||
{"checked_at": "2026-10-14T16:47:09-04:00", "duration_s": 60,
|
||||
"dhcp_guard": {"state": "allowed"|"blocked"|"unknown", "reason": "..."},
|
||||
"interfaces": [{"iface": "br-pxe", "mac": "aa:bb:..", "role": "imaging"|"ge"|"other",
|
||||
"link": "up"|"down", "speed_mbps": 5000, "addrs": ["172.16.9.1/24"],
|
||||
"verdict": "isolated"|"ge"|"foreign-dhcp"|"no-link"|"error", "evidence": ["..."],
|
||||
"dhcp_servers": [{"server": "10.1.2.3", "domain": "x.geaerospace.net", "own": false}],
|
||||
"switch": {"lldp": [{"system": "...", "port": "...", "vlan": null}],
|
||||
"cdp": [{"device": "...", "port": "..."}], "eapol": false, "stp": false},
|
||||
"arp_hosts": 3, "dnsmasq_bound": true}]}
|
||||
|
||||
`dnsmasq_bound` is null when it cannot be told (no `ss`). A bridge
|
||||
(br-pxe) is reported as one interface; its ports are listened on too,
|
||||
because LLDP and 802.1X frames stop at the port and never reach the bridge.
|
||||
414
playbook/FlatUnattendW10-engineer.xml
Normal file
414
playbook/FlatUnattendW10-engineer.xml
Normal file
@@ -0,0 +1,414 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<!-- 1. windowsPE is intentionally empty -->
|
||||
<settings pass="windowsPE" />
|
||||
|
||||
<!-- 2. Offline servicing (drivers) -->
|
||||
<settings pass="offlineServicing">
|
||||
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<DriverPaths>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
|
||||
<Path>W:\Drivers</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\BROTHER\UNIV-PS-01181\PS\64</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\HP</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\XEROX\UNIV_5.1035.2.0_PS_x64_Driver</Path>
|
||||
</PathAndCredentials>
|
||||
</DriverPaths>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- 3. specialize: computer naming + RunSynchronous all in ONE component -->
|
||||
<settings pass="specialize">
|
||||
<!-- 3a. Shell-Setup for naming/owner/org -->
|
||||
<component name="Microsoft-Windows-Shell-Setup"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<ComputerName>H%serialnumber%</ComputerName>
|
||||
<RegisteredOrganization>GE Aerospace</RegisteredOrganization>
|
||||
<RegisteredOwner>GE</RegisteredOwner>
|
||||
<TimeZone>Eastern Standard Time</TimeZone>
|
||||
</component>
|
||||
|
||||
<!-- 3b. RunSynchronous for all of your installers, copies, etc. -->
|
||||
<component name="Microsoft-Windows-Deployment"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<RunSynchronous>
|
||||
|
||||
<!-- INTERNETACCESS bootstrap profile. BLUESSO and EAP-PEAP were removed:
|
||||
the MCL ppkg installs both, and BLUESSO cannot bootstrap (hidden
|
||||
SSID, manual connect, user password). wlansvc must be started
|
||||
first or netsh silently fails. -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>cmd /c sc config wlansvc start= auto & net start wlansvc & exit /b 0</Path>
|
||||
<Description>Start WLAN AutoConfig so the WiFi profile can be added</Description>
|
||||
</RunSynchronousCommand>
|
||||
<!-- PXE imaging floor progress (best-effort, never fails setup). -Persist copies the script
|
||||
out of C:\Deploy, which the bulk-enrollment task deletes before first logon.
|
||||
RunSynchronous Path max 259 chars: check with scripts/lint-unattend.py (local mode). -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>powershell.exe -NoP -EP Bypass -File C:\Deploy\Applications\extra\pxe-status\Send-ImagingStage.ps1 -Persist -PCType gea-engineer -StageIndex 2 -Stage "Windows setup: installing apps"</Path>
|
||||
<Description>PXE status: stage 2 of 5</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Path>cmd /c netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\WiFi-Profile.xml" user=all</Path>
|
||||
<Description>Add generic WiFi profile</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Certificates -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_External_Root_CA_2_1.cer"</Path>
|
||||
<Description>Install External Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_External_Intermediate_CA_2_1.cer"</Path>
|
||||
<Description>Install External Intermediate Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_Enterprise_Root_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Device_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Device Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Server_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Server Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Smart_Card_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install SmartCard Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>10</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_User_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install User Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>11</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_Aerospace_Enterprise_Root_CA_1.cer"</Path>
|
||||
<Description>Install Aerospace Enterprise Root CA</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>12</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\ZscalerCommercialCertificate-2048-SHA256.crt"</Path>
|
||||
<Description>Install Zscaler Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Fonts -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>13</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\3OF9.TTF" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy 3OF9 Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>14</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "3 of 9 Barcode" /t REG_SZ /d "3OF9.TTF" /f</Path>
|
||||
<Description>Register 3OF9 Barcode Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>15</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\Code39AzaleaNarrow3.ttf" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy Code39 Azalea Narrow Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>16</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Code39AzaleaNarrow3" /t REG_SZ /d "Code39AzaleaNarrow3.ttf" /f</Path>
|
||||
<Description>Register Code39 Azalea Narrow Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>17</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\Code39Azalea.ttf" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy Code39 Azalea Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>18</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Code39Azalea" /t REG_SZ /d "Code39Azalea.ttf" /f</Path>
|
||||
<Description>Register Code39 Azalea Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- OpenText installers -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>19</Order>
|
||||
<Path>cmd /c "C:\Deploy\Applications\extra\opentext\Setup-OpenText.cmd"</Path>
|
||||
<Description>Install OpenText HostExplorer SP1 (shopfloor method)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>20</Order>
|
||||
<Path>C:\Deploy\Applications\extra\opentext\J2SE_Runtime_Environment_1.6.0_22_Static_Config_V2_Co-Exist.EXE /silent /norestart</Path>
|
||||
<Description>Install J2SE Runtime Environment 1.6.0_22</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Adobe -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>22</Order>
|
||||
<Path>msiexec /i "C:\Deploy\Applications\extra\adobe\AcroRead.msi" TRANSFORMS="C:\Deploy\Applications\extra\adobe\AcroRead.mst" /quiet /norestart</Path>
|
||||
<Description>Install Adobe</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>23</Order>
|
||||
<Path>msiexec /p "C:\Deploy\Applications\extra\adobe\AcroRdrDCUpd2500120531.msp" /quiet /norestart</Path>
|
||||
<Description>Apply Adobe Reader Update</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Java JRE 8 Enterprise Installation -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>26</Order>
|
||||
<Path>C:\Deploy\Applications\extra\java\jre-8u441-windows-i586.exe /s INSTALLDIR="C:\Program Files (x86)\Java\jre1.8.0_441" STATIC=1 AUTO_UPDATE=0 REBOOT=0 SPONSORS=0 WEB_JAVA=0 /L "C:\Windows\Logs\java-install.log"</Path>
|
||||
<Description>Install Java JRE 8 Update 441 with Enterprise Settings and Logging</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Updates via Registry (64-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>27</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\JavaSoft\Java Update\Policy" /v EnableJavaUpdate /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Updates (64-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Updates via Registry (32-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>28</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Wow6432Node\JavaSoft\Java Update\Policy" /v EnableJavaUpdate /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Updates (32-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Update Check (64-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>29</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\JavaSoft\Java Update\Policy" /v EnableAutoUpdateCheck /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Update Check (64-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Update Check (32-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>30</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Wow6432Node\JavaSoft\Java Update\Policy" /v EnableAutoUpdateCheck /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Update Check (32-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Cortana -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>31</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v AllowCortana /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Cortana</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Web Search in Start Menu -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>33</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v DisableWebSearch /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable Web Search in Start Menu</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Bing / web search suggestions in Start menu.
|
||||
BingSearchEnabled is documented at HKCU\Software\Microsoft\Windows\CurrentVersion\Search
|
||||
(not HKLM), so the old HKLM policy placement was a no-op. The current documented
|
||||
suppression is DisableSearchBoxSuggestions=1 at Software\Policies\Microsoft\Windows\Explorer.
|
||||
Writing it into the Default User hive so every newly-created account inherits it. -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>34</Order>
|
||||
<Path>cmd /c reg load HKU\TempDU C:\Users\Default\NTUSER.DAT & reg add "HKU\TempDU\Software\Policies\Microsoft\Windows\Explorer" /v DisableSearchBoxSuggestions /t REG_DWORD /d 1 /f & reg unload HKU\TempDU</Path>
|
||||
<Description>Disable Bing/web search suggestions (Default User hive)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Search Web when searching Windows -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>35</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v ConnectedSearchUseWeb /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Connected Search Use Web</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Cortana Speech Recognition -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>36</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\InputPersonalization" /v AllowInputPersonalization /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Cortana Speech Recognition</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Search Highlights (news/trending) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>38</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v EnableDynamicContentInWSB /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Search Highlights</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>37</Order>
|
||||
<Path>powershell.exe -NoP -EP Bypass -File "C:\Deploy\Applications\extra_engineering\Install-EngineeringLayer.ps1" -StageOnly</Path>
|
||||
<!-- Copies the Engineering Layer payload and the script itself to
|
||||
C:\PreInstall\engineering now, while C:\Deploy certainly exists. The
|
||||
first-logon install (oobeSystem Order 11) runs from there: a PC that
|
||||
reboots before the employee signs in has lost C:\Deploy to the
|
||||
orchestrator's DeleteDeployDir task by then. -->
|
||||
<Description>Engineering Layer: stage payload out of C:\Deploy</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>39</Order>
|
||||
<Path>powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Deploy\Applications\BPRT\Register-BulkEnrollTask-Standard.ps1"</Path>
|
||||
<Description>Add bulk enrollment orchestrator to Run key</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>40</Order>
|
||||
<Path>reg.exe load HKU\TempDefault C:\Users\Default\NTUSER.DAT</Path>
|
||||
<Description>Load Default User registry hive</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>41</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable startup delay for all new users</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>42</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v WaitForIdleState /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable wait for idle state for all new users</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>43</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v StartShownOnUpgrade /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Prevent Start menu from opening on first login</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>44</Order>
|
||||
<Path>reg.exe unload HKU\TempDefault</Path>
|
||||
<Description>Unload Default User registry hive</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>45</Order>
|
||||
<Path>reg add HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU /v NoAutoUpdate /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable automatic Windows updates (NoAutoUpdate)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>46</Order>
|
||||
<Path>reg add HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU /v AUOptions /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable automatic Windows updates (AUOptions)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>47</Order>
|
||||
<Path>reg.exe add "HKLM\Software\Microsoft\Command Processor" /v AutoRun /t REG_SZ /d "doskey vanilla=powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\Deploy\Applications\BPRT\Show-VanillaConfirmation.ps1" /f</Path>
|
||||
<Description>Define vanilla CMD alias for manual OOBE fallback via Shift+F10</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>48</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices" /v Deny_All /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Block all removable media reads to prevent PPKG auto-detection at OOBE</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>49</Order>
|
||||
<Path>powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-engineer -StageIndex 3 -Status succeeded -Stage "Ready at OOBE - hand to the employee"</Path>
|
||||
<Description>PXE status: stage 3 of 3, done (the rest needs the employee to log in)</Description>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- 4. oobeSystem: hide OEM/EULA screens -->
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-Shell-Setup"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||
<HideOnlineAccountScreens>false</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||
<NetworkLocation>Work</NetworkLocation>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
<SkipUserOOBE>false</SkipUserOOBE>
|
||||
<SkipMachineOOBE>false</SkipMachineOOBE>
|
||||
</OOBE>
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-engineer -StageIndex 4 -Stage "First logon: installing engineering layer"</CommandLine>
|
||||
<Description>PXE status: stage 4 of 5</Description>
|
||||
<Order>1</Order>
|
||||
</SynchronousCommand>
|
||||
|
||||
|
||||
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>cmd /c sc config wuauserv start= disabled</CommandLine>
|
||||
<Description>disable windows update</Description>
|
||||
<Order>4</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>cmd /c sc stop wuauserv</CommandLine>
|
||||
<Description>disable windows update</Description>
|
||||
<Order>5</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe load HKU\TempDefault C:\Users\Default\NTUSER.DAT</CommandLine>
|
||||
<Description>Load Default User registry hive to reverse startup delay</Description>
|
||||
<Order>6</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /f</CommandLine>
|
||||
<Description>Remove startup delay override from Default User</Description>
|
||||
<Order>7</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v WaitForIdleState /f</CommandLine>
|
||||
<Description>Remove wait for idle state override from Default User</Description>
|
||||
<Order>8</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe unload HKU\TempDefault</CommandLine>
|
||||
<Description>Unload Default User registry hive</Description>
|
||||
<Order>9</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices" /v Deny_All /f</CommandLine>
|
||||
<Description>Restore removable media access after OOBE PPKG block</Description>
|
||||
<Order>10</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>11</Order>
|
||||
<CommandLine>cmd /c if exist "C:\PreInstall\engineering\Install-EngineeringLayer.ps1" (powershell.exe -NoP -EP Bypass -File "C:\PreInstall\engineering\Install-EngineeringLayer.ps1") else (powershell.exe -NoP -EP Bypass -File "C:\Deploy\Applications\extra_engineering\Install-EngineeringLayer.ps1")</CommandLine>
|
||||
<!-- Installs from the copy specialize Order 37 staged in
|
||||
C:\PreInstall\engineering, so it works after a reboot has removed
|
||||
C:\Deploy (DeleteDeployDir). Falls back to C:\Deploy if staging did not
|
||||
happen. The installer (.NET 3.5, VC++, Cygwin, DUU) can reboot part way
|
||||
through; the script is idempotent and marker-gated, and it makes sure
|
||||
the "Configure my Engineering Layer" icon reaches the employee.
|
||||
Description is capped at 256 chars, so the detail lives here. -->
|
||||
<Description>Engineering Layer: install from the staged copy</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-engineer -StageIndex 5 -Stage "Setup complete - enrollment continues on the PC" -Status succeeded</CommandLine>
|
||||
<Description>PXE status: stage 5 of 5</Description>
|
||||
<Order>12</Order>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
</unattend>
|
||||
@@ -1,196 +1,288 @@
|
||||
<?xml version="1.0"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||
<settings pass="windowsPE">
|
||||
</settings>
|
||||
<settings pass="offlineServicing">
|
||||
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<DriverPaths>
|
||||
<PathAndCredentials xmlns="" wcm:keyValue="1" action="add">
|
||||
<Path>W:\Drivers</Path>
|
||||
</PathAndCredentials>
|
||||
</DriverPaths>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<ComputerName>H%serialnumber%</ComputerName>
|
||||
<RegisteredOrganization>GE Aerospace</RegisteredOrganization>
|
||||
<RegisteredOwner>GE</RegisteredOwner>
|
||||
<TimeZone>Eastern Standard Time</TimeZone>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<RunSynchronous>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install External Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Intermediate_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install External Intermediate Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install Enterprise Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Device_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise Device Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Server_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise Server Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Smart_Card_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise SmartCard Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_User_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise User Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Aerospace_Enterprise_Root_CA_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install Aerospace Enterprise Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\ZscalerCommercialCertificate-2048-SHA256.crt' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install Zscaler Commercial Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Path>reg.exe add "HKLM\System\CurrentControlSet\Control\Network\NewNetworkWindowOff" /f </Path>
|
||||
<Description>Disable Network Windows</Description>
|
||||
<Order>10</Order>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>11</Order>
|
||||
<Path>netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\WiFi-Profile.xml" user=all</Path>
|
||||
<Description>Install INTERNETACCESS WiFi Profile</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>12</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Enable-PSRemoting -Force -SkipNetworkProfileCheck"</Path>
|
||||
<Description>Enable WinRM</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>13</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Bypass OOBE network requirement</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>14</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipMachineOOBE /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Skip machine OOBE phase</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>15</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipUserOOBE /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Skip user OOBE phase</Description>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-International-Core" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<InputLocale>en-US</InputLocale>
|
||||
<SystemLocale>en-US</SystemLocale>
|
||||
<UILanguage>en-US</UILanguage>
|
||||
<UserLocale>en-US</UserLocale>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
</OOBE>
|
||||
<UserAccounts>
|
||||
<LocalAccounts>
|
||||
<LocalAccount wcm:action="add">
|
||||
<Password>
|
||||
<Value>Pa55word</Value>
|
||||
<PlainText>true</PlainText>
|
||||
</Password>
|
||||
<Name>SupportUser</Name>
|
||||
<Group>Administrators</Group>
|
||||
<DisplayName>SupportUser</DisplayName>
|
||||
</LocalAccount>
|
||||
</LocalAccounts>
|
||||
</UserAccounts>
|
||||
<AutoLogon>
|
||||
<Password>
|
||||
<Value>Pa55word</Value>
|
||||
<PlainText>true</PlainText>
|
||||
</Password>
|
||||
<Enabled>true</Enabled>
|
||||
<Username>SupportUser</Username>
|
||||
<LogonCount>7</LogonCount>
|
||||
</AutoLogon>
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<CommandLine>cmd.exe /c powercfg /change monitor-timeout-ac 0 & powercfg /change monitor-timeout-dc 0 & powercfg /change standby-timeout-ac 0 & powercfg /change standby-timeout-dc 0</CommandLine>
|
||||
<Description>Disable display and sleep timeout during setup</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -match 'Wi-Fi|Wireless' } | Set-NetIPInterface -InterfaceMetric 10; Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Set-NetIPInterface -InterfaceMetric 100"</CommandLine>
|
||||
<Description>Prioritize WiFi over ethernet</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private"</CommandLine>
|
||||
<Description>Set network profile to Private</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Fetch-StagingPayload.ps1"</CommandLine>
|
||||
<Description>Fetch bulk staging (shopfloor-setup tree + preinstall bundle) from the PXE share on a fresh mount, BEFORE the production-network switch takes the bay off the imaging LAN. Detailed log at C:\Logs\Fetch\.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Verify-And-Heal-Staging.ps1"</CommandLine>
|
||||
<Description>Verify every imaging payload arrived and re-pull anything missing from the PXE share (incl the CMM bundle + selected-bay backup) while still on the imaging LAN, BEFORE wait-for-internet switches the bay to the production network. Log at C:\Logs\Fetch\.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\wait-for-internet.ps1"</CommandLine>
|
||||
<Description>Prompt to connect production network then wait for TCP 443 connectivity</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\migrate-to-wifi.ps1"</CommandLine>
|
||||
<Description>Migrate from wired to WiFi if WiFi adapter present, else stay on wired</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<CommandLine>msiexec.exe /i "C:\PreInstall\installers\powershell7\PowerShell-7.5.4-win-x64.msi" /qn /norestart ADD_PATH=1 USE_MU=0 ENABLE_MU=0 DISABLE_TELEMETRY=1</CommandLine>
|
||||
<Description>Install PowerShell 7 BEFORE PPKG so Intune SetupCredentials Win32App finds pwsh.exe (race fix)</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\run-enrollment.ps1"</CommandLine>
|
||||
<Description>Run GCCH Enrollment</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>10</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
|
||||
<Description>Run shopfloor PC type setup</Description>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
<TimeZone>Eastern Standard Time</TimeZone>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
<?xml version="1.0"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend">
|
||||
<settings pass="windowsPE">
|
||||
</settings>
|
||||
<settings pass="offlineServicing">
|
||||
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<DriverPaths>
|
||||
<PathAndCredentials xmlns="" wcm:keyValue="1" action="add">
|
||||
<Path>W:\Drivers</Path>
|
||||
</PathAndCredentials>
|
||||
</DriverPaths>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="specialize">
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<ComputerName>H%serialnumber%</ComputerName>
|
||||
<RegisteredOrganization>GE Aerospace</RegisteredOrganization>
|
||||
<RegisteredOwner>GE</RegisteredOwner>
|
||||
<TimeZone>Eastern Standard Time</TimeZone>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Deployment" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<RunSynchronous>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install External Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_External_Intermediate_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install External Intermediate Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Root_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install Enterprise Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Device_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise Device Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Server_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise Server Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_Smart_Card_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise SmartCard Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Enterprise_User_Issuing_CA_2_1.cer' -CertStoreLocation 'Cert:\LocalMachine\CA'"</Path>
|
||||
<Description>Install Enterprise User Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\GE_Aerospace_Enterprise_Root_CA_1.cer' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install Aerospace Enterprise Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Import-Certificate -FilePath 'C:\Deploy\Applications\ZscalerCommercialCertificate-2048-SHA256.crt' -CertStoreLocation 'Cert:\LocalMachine\Root'"</Path>
|
||||
<Description>Install Zscaler Commercial Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Path>reg.exe add "HKLM\System\CurrentControlSet\Control\Network\NewNetworkWindowOff" /f </Path>
|
||||
<Description>Disable Network Windows</Description>
|
||||
<Order>10</Order>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>11</Order>
|
||||
<Path>netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\WiFi-Profile.xml" user=all</Path>
|
||||
<Description>Install INTERNETACCESS WiFi Profile</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>12</Order>
|
||||
<Path>powershell.exe -ExecutionPolicy Bypass -Command "Enable-PSRemoting -Force -SkipNetworkProfileCheck"</Path>
|
||||
<Description>Enable WinRM</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>13</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v BypassNRO /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Bypass OOBE network requirement</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>14</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipMachineOOBE /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Skip machine OOBE phase</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>15</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\OOBE" /v SkipUserOOBE /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Skip user OOBE phase</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>17</Order>
|
||||
<Path>reg.exe load HKU\TempDefault C:\Users\Default\NTUSER.DAT</Path>
|
||||
<Description>Load Default User registry hive</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>18</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable startup delay for all new users</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>19</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v WaitForIdleState /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable wait for idle state for all new users</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>20</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v StartShownOnUpgrade /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Prevent Start menu from opening on first login</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>21</Order>
|
||||
<Path>reg.exe unload HKU\TempDefault</Path>
|
||||
<Description>Unload Default User registry hive</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>22</Order>
|
||||
<Path>reg add HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU /v NoAutoUpdate /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable automatic Windows updates (NoAutoUpdate)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>23</Order>
|
||||
<Path>reg add HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU /v AUOptions /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable automatic Windows updates (AUOptions)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>24</Order>
|
||||
<Path>reg.exe add "HKLM\Software\Microsoft\Command Processor" /v AutoRun /t REG_SZ /d "doskey vanilla=powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\Deploy\Applications\BPRT\Show-VanillaConfirmation.ps1" /f</Path>
|
||||
<Description>Define vanilla CMD alias for manual OOBE fallback via Shift+F10</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>25</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices" /v Deny_All /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Block all removable media reads to prevent PPKG auto-detection at OOBE</Description>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-International-Core" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<InputLocale>en-US</InputLocale>
|
||||
<SystemLocale>en-US</SystemLocale>
|
||||
<UILanguage>en-US</UILanguage>
|
||||
<UserLocale>en-US</UserLocale>
|
||||
</component>
|
||||
<component name="Microsoft-Windows-Shell-Setup" processorArchitecture="*arch*" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||
<HideOnlineAccountScreens>true</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
</OOBE>
|
||||
<UserAccounts>
|
||||
<LocalAccounts>
|
||||
<LocalAccount wcm:action="add">
|
||||
<Password>
|
||||
<Value>Pa55word</Value>
|
||||
<PlainText>true</PlainText>
|
||||
</Password>
|
||||
<Name>SupportUser</Name>
|
||||
<Group>Administrators</Group>
|
||||
<DisplayName>SupportUser</DisplayName>
|
||||
</LocalAccount>
|
||||
</LocalAccounts>
|
||||
</UserAccounts>
|
||||
<AutoLogon>
|
||||
<Password>
|
||||
<Value>Pa55word</Value>
|
||||
<PlainText>true</PlainText>
|
||||
</Password>
|
||||
<Enabled>true</Enabled>
|
||||
<Username>SupportUser</Username>
|
||||
<LogonCount>12</LogonCount>
|
||||
</AutoLogon>
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<CommandLine>cmd.exe /c powercfg /change monitor-timeout-ac 0 & powercfg /change monitor-timeout-dc 0 & powercfg /change standby-timeout-ac 0 & powercfg /change standby-timeout-dc 0</CommandLine>
|
||||
<Description>Disable display and sleep timeout during setup</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -match 'Wi-Fi|Wireless' } | Set-NetIPInterface -InterfaceMetric 10; Get-NetAdapter -Physical | Where-Object { $_.InterfaceDescription -notmatch 'Wi-Fi|Wireless' } | Set-NetIPInterface -InterfaceMetric 100"</CommandLine>
|
||||
<Description>Prioritize WiFi over ethernet</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -Command "Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private"</CommandLine>
|
||||
<Description>Set network profile to Private</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<CommandLine>powershell.exe -ep Bypass -Command "net use Z: \\172.16.9.1\enrollment /user:pxe-upload pxe; robocopy Z:\shopfloor-setup C:\Enrollment Fetch-StagingPayload.ps1 Verify-And-Heal-Staging.ps1 /R:2 /W:2; robocopy Z:\scripts C:\Enrollment run-enrollment.ps1 wait-for-internet.ps1 migrate-to-wifi.ps1 /R:2 /W:2"</CommandLine>
|
||||
<Description>Bootstrap self-heal: pull Fetch/Verify-Heal + enrollment scripts from the PXE share so Order 5/6 run after an early WinPE staging failure.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Fetch-StagingPayload.ps1"</CommandLine>
|
||||
<Description>Fetch bulk staging (shopfloor-setup tree + preinstall bundle) from the PXE share on a fresh mount, BEFORE the production-network switch takes the bay off the imaging LAN. Detailed log at C:\Logs\Fetch\.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Verify-And-Heal-Staging.ps1"</CommandLine>
|
||||
<Description>Verify + re-pull any missing imaging payload (shopfloor setup scripts, site app lists and installers) from the PXE share on the imaging LAN before the production-network switch. Log C:\Logs\Fetch.</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\wait-for-internet.ps1"</CommandLine>
|
||||
<Description>Prompt to connect production network then wait for TCP 443 connectivity</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\migrate-to-wifi.ps1"</CommandLine>
|
||||
<Description>Migrate from wired to WiFi if WiFi adapter present, else stay on wired</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<CommandLine>msiexec.exe /i "C:\PreInstall\installers\powershell7\PowerShell-7.5.4-win-x64.msi" /qn /norestart ADD_PATH=1 USE_MU=0 ENABLE_MU=0 DISABLE_TELEMETRY=1</CommandLine>
|
||||
<Description>Install PowerShell 7 BEFORE PPKG so Intune SetupCredentials Win32App finds pwsh.exe (race fix)</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>10</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\run-enrollment.ps1"</CommandLine>
|
||||
<!-- run-enrollment.ps1 lives in C:\Enrollment: startnet stages it and
|
||||
the .ppkg there. The earlier C:\run-enrollment.ps1 path pointed at a
|
||||
file that never existed, so this step silently did nothing.
|
||||
Keep prose in comments - Description is capped at 256 chars and an
|
||||
over-length one invalidates the whole answer file for its pass. -->
|
||||
<Description>Run GCCH Enrollment</Description>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>11</Order>
|
||||
<CommandLine>powershell.exe -ExecutionPolicy Bypass -File "C:\Enrollment\Run-ShopfloorSetup.ps1"</CommandLine>
|
||||
<Description>Run shopfloor PC type setup</Description>
|
||||
</SynchronousCommand>
|
||||
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>cmd /c sc config wuauserv start= disabled</CommandLine>
|
||||
<Description>disable windows update</Description>
|
||||
<Order>12</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>cmd /c sc stop wuauserv</CommandLine>
|
||||
<Description>disable windows update</Description>
|
||||
<Order>13</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe load HKU\TempDefault C:\Users\Default\NTUSER.DAT</CommandLine>
|
||||
<Description>Load Default User registry hive to reverse startup delay</Description>
|
||||
<Order>14</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /f</CommandLine>
|
||||
<Description>Remove startup delay override from Default User</Description>
|
||||
<Order>15</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v WaitForIdleState /f</CommandLine>
|
||||
<Description>Remove wait for idle state override from Default User</Description>
|
||||
<Order>16</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe unload HKU\TempDefault</CommandLine>
|
||||
<Description>Unload Default User registry hive</Description>
|
||||
<Order>17</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices" /v Deny_All /f</CommandLine>
|
||||
<Description>Restore removable media access after OOBE PPKG block</Description>
|
||||
<Order>18</Order>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
<TimeZone>Eastern Standard Time</TimeZone>
|
||||
</component>
|
||||
</settings>
|
||||
</unattend>
|
||||
|
||||
@@ -1,307 +1,392 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<!-- 1. windowsPE is intentionally empty -->
|
||||
<settings pass="windowsPE" />
|
||||
|
||||
<!-- 2. Offline servicing (drivers) -->
|
||||
<settings pass="offlineServicing">
|
||||
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<DriverPaths>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
|
||||
<Path>W:\Drivers</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\BROTHER\UNIV-PS-01181\PS\64</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\HP</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\XEROX\UNIV_5.1035.2.0_PS_x64_Driver</Path>
|
||||
</PathAndCredentials>
|
||||
</DriverPaths>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- 3. specialize: computer naming + RunSynchronous all in ONE component -->
|
||||
<settings pass="specialize">
|
||||
<!-- 3a. Shell-Setup for naming/owner/org -->
|
||||
<component name="Microsoft-Windows-Shell-Setup"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<ComputerName>H%serialnumber%</ComputerName>
|
||||
<RegisteredOrganization>GE Aerospace</RegisteredOrganization>
|
||||
<RegisteredOwner>GE</RegisteredOwner>
|
||||
<TimeZone>Eastern Standard Time</TimeZone>
|
||||
</component>
|
||||
|
||||
<!-- 3b. RunSynchronous for all of your installers, copies, etc. -->
|
||||
<component name="Microsoft-Windows-Deployment"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<RunSynchronous>
|
||||
<!-- EAP-PEAP MSI -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>msiexec /i "C:\Deploy\Applications\extra\wireless\EAP-PEAP.msi" /quiet /norestart</Path>
|
||||
<Description>Install EAP-PEAP</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Wi-Fi profiles -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>cmd /c netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\BLUESSO.xml" user=all</Path>
|
||||
<Description>Add BLUESSO WiFi profile</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Path>cmd /c netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\WiFi-Profile.xml" user=all</Path>
|
||||
<Description>Add generic WiFi profile</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Certificates -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_External_Root_CA_2_1.cer"</Path>
|
||||
<Description>Install External Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_External_Intermediate_CA_2_1.cer"</Path>
|
||||
<Description>Install External Intermediate Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_Enterprise_Root_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Device_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Device Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Server_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Server Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Smart_Card_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install SmartCard Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>10</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_User_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install User Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>11</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_Aerospace_Enterprise_Root_CA_1.cer"</Path>
|
||||
<Description>Install Aerospace Enterprise Root CA</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>12</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\ZscalerCommercialCertificate-2048-SHA256.crt"</Path>
|
||||
<Description>Install Zscaler Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Fonts -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>13</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\3OF9.TTF" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy 3OF9 Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>14</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "3 of 9 Barcode" /t REG_SZ /d "3OF9.TTF" /f</Path>
|
||||
<Description>Register 3OF9 Barcode Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>15</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\Code39AzaleaNarrow3.ttf" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy Code39 Azalea Narrow Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>16</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Code39AzaleaNarrow3" /t REG_SZ /d "Code39AzaleaNarrow3.ttf" /f</Path>
|
||||
<Description>Register Code39 Azalea Narrow Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>17</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\Code39Azalea.ttf" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy Code39 Azalea Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>18</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Code39Azalea" /t REG_SZ /d "Code39Azalea.ttf" /f</Path>
|
||||
<Description>Register Code39 Azalea Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- OpenText installers -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>19</Order>
|
||||
<Path>C:\Deploy\Applications\extra\opentext\opentext_hostexplorer_sp1_15.0_v01.exe /quiet /norestart</Path>
|
||||
<Description>Install OpenText HostExplorer SP1</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>20</Order>
|
||||
<Path>C:\Deploy\Applications\extra\opentext\J2SE_Runtime_Environment_1.6.0_22_Static_Config_V2_Co-Exist.EXE /silent /norestart</Path>
|
||||
<Description>Install J2SE Runtime Environment 1.6.0_22</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>21</Order>
|
||||
<Path>C:\Deploy\Applications\extra\opentext\unattended.bat</Path>
|
||||
<Description>Install J2SE Runtime Environment 1.6.0_22</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Adobe -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>22</Order>
|
||||
<Path>msiexec /i "C:\Deploy\Applications\extra\adobe\AcroRead.msi" TRANSFORMS="C:\Deploy\Applications\extra\adobe\AcroRead.mst" /quiet /norestart</Path>
|
||||
<Description>Install Adobe</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>23</Order>
|
||||
<Path>msiexec /p "C:\Deploy\Applications\extra\adobe\AcroRdrDCUpd2500120531.msp" /quiet /norestart</Path>
|
||||
<Description>Apply Adobe Reader Update</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Java JRE 8 Enterprise Installation -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>26</Order>
|
||||
<Path>C:\Deploy\Applications\extra\java\jre-8u441-windows-i586.exe /s INSTALLDIR="C:\Program Files (x86)\Java\jre1.8.0_441" STATIC=1 AUTO_UPDATE=0 REBOOT=0 SPONSORS=0 WEB_JAVA=0 /L "C:\Windows\Logs\java-install.log"</Path>
|
||||
<Description>Install Java JRE 8 Update 441 with Enterprise Settings and Logging</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Updates via Registry (64-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>27</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\JavaSoft\Java Update\Policy" /v EnableJavaUpdate /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Updates (64-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Updates via Registry (32-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>28</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Wow6432Node\JavaSoft\Java Update\Policy" /v EnableJavaUpdate /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Updates (32-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Update Check (64-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>29</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\JavaSoft\Java Update\Policy" /v EnableAutoUpdateCheck /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Update Check (64-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Update Check (32-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>30</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Wow6432Node\JavaSoft\Java Update\Policy" /v EnableAutoUpdateCheck /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Update Check (32-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Cortana -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>31</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v AllowCortana /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Cortana</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Web Search in Start Menu -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>33</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v DisableWebSearch /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable Web Search in Start Menu</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Bing / web search suggestions in Start menu.
|
||||
BingSearchEnabled is documented at HKCU\Software\Microsoft\Windows\CurrentVersion\Search
|
||||
(not HKLM), so the old HKLM policy placement was a no-op. The current documented
|
||||
suppression is DisableSearchBoxSuggestions=1 at Software\Policies\Microsoft\Windows\Explorer.
|
||||
Writing it into the Default User hive so every newly-created account inherits it. -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>34</Order>
|
||||
<Path>cmd /c reg load HKU\TempDU C:\Users\Default\NTUSER.DAT & reg add "HKU\TempDU\Software\Policies\Microsoft\Windows\Explorer" /v DisableSearchBoxSuggestions /t REG_DWORD /d 1 /f & reg unload HKU\TempDU</Path>
|
||||
<Description>Disable Bing/web search suggestions (Default User hive)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Search Web when searching Windows -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>35</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v ConnectedSearchUseWeb /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Connected Search Use Web</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Cortana Speech Recognition -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>36</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\InputPersonalization" /v AllowInputPersonalization /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Cortana Speech Recognition</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Search Highlights (news/trending) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>38</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v EnableDynamicContentInWSB /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Search Highlights</Description>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- 4. oobeSystem: hide OEM/EULA screens -->
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-Shell-Setup"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||
<HideOnlineAccountScreens>false</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>false</HideWirelessSetupInOOBE>
|
||||
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||
<NetworkLocation>Work</NetworkLocation>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
<SkipUserOOBE>false</SkipUserOOBE>
|
||||
<SkipMachineOOBE>false</SkipMachineOOBE>
|
||||
</OOBE>
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<CommandLine>shutdown -a</CommandLine>
|
||||
<Description>Cancel any scheduled shutdown from Office installation</Description>
|
||||
</SynchronousCommand>
|
||||
|
||||
<!-- Install Zscaler -->
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<CommandLine>C:\Deploy\Applications\extra\zscaler\zscaler.bat</CommandLine>
|
||||
<Description>Install Zscaler Client Connector</Description>
|
||||
</SynchronousCommand>
|
||||
|
||||
<!-- Install Microsoft Office -->
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<CommandLine>cmd /c "cd /d C:\Deploy\Applications\extra\office\ && install.bat"</CommandLine>
|
||||
<Description>Install Microsoft Office</Description>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
</unattend>
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<unattend xmlns="urn:schemas-microsoft-com:unattend"
|
||||
xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
|
||||
<!-- 1. windowsPE is intentionally empty -->
|
||||
<settings pass="windowsPE" />
|
||||
|
||||
<!-- 2. Offline servicing (drivers) -->
|
||||
<settings pass="offlineServicing">
|
||||
<component name="Microsoft-Windows-PnpCustomizationsNonWinPE"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<DriverPaths>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="1">
|
||||
<Path>W:\Drivers</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="2">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\BROTHER\UNIV-PS-01181\PS\64</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="3">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\HP</Path>
|
||||
</PathAndCredentials>
|
||||
<PathAndCredentials wcm:action="add" wcm:keyValue="4">
|
||||
<Path>W:\Deploy\Applications\extra\printdrivers\XEROX\UNIV_5.1035.2.0_PS_x64_Driver</Path>
|
||||
</PathAndCredentials>
|
||||
</DriverPaths>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- 3. specialize: computer naming + RunSynchronous all in ONE component -->
|
||||
<settings pass="specialize">
|
||||
<!-- 3a. Shell-Setup for naming/owner/org -->
|
||||
<component name="Microsoft-Windows-Shell-Setup"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<ComputerName>H%serialnumber%</ComputerName>
|
||||
<RegisteredOrganization>GE Aerospace</RegisteredOrganization>
|
||||
<RegisteredOwner>GE</RegisteredOwner>
|
||||
<TimeZone>Eastern Standard Time</TimeZone>
|
||||
</component>
|
||||
|
||||
<!-- 3b. RunSynchronous for all of your installers, copies, etc. -->
|
||||
<component name="Microsoft-Windows-Deployment"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<RunSynchronous>
|
||||
|
||||
<!-- INTERNETACCESS bootstrap profile. BLUESSO and EAP-PEAP were removed:
|
||||
the MCL ppkg installs both, and BLUESSO cannot bootstrap (hidden
|
||||
SSID, manual connect, user password). wlansvc must be started
|
||||
first or netsh silently fails. -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>1</Order>
|
||||
<Path>cmd /c sc config wlansvc start= auto & net start wlansvc & exit /b 0</Path>
|
||||
<Description>Start WLAN AutoConfig so the WiFi profile can be added</Description>
|
||||
</RunSynchronousCommand>
|
||||
<!-- PXE imaging floor progress (best-effort, never fails setup). -Persist copies the script
|
||||
out of C:\Deploy, which the bulk-enrollment task deletes before first logon.
|
||||
RunSynchronous Path max 259 chars: check with scripts/lint-unattend.py (local mode). -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>2</Order>
|
||||
<Path>powershell.exe -NoP -EP Bypass -File C:\Deploy\Applications\extra\pxe-status\Send-ImagingStage.ps1 -Persist -PCType gea-standard -StageIndex 2 -Stage "Windows setup: installing apps"</Path>
|
||||
<Description>PXE status: stage 2 of 5</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>3</Order>
|
||||
<Path>cmd /c netsh wlan add profile filename="C:\Deploy\Applications\extra\wireless\WiFi-Profile.xml" user=all</Path>
|
||||
<Description>Add generic WiFi profile</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Certificates -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>4</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_External_Root_CA_2_1.cer"</Path>
|
||||
<Description>Install External Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>5</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_External_Intermediate_CA_2_1.cer"</Path>
|
||||
<Description>Install External Intermediate Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>6</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_Enterprise_Root_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Root Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>7</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Device_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Device Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>8</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Server_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install Enterprise Server Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>9</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_Smart_Card_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install SmartCard Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>10</Order>
|
||||
<Path>cmd /c certutil -addstore CA "C:\Deploy\Applications\GE_Enterprise_User_Issuing_CA_2_1.cer"</Path>
|
||||
<Description>Install User Issuing Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>11</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\GE_Aerospace_Enterprise_Root_CA_1.cer"</Path>
|
||||
<Description>Install Aerospace Enterprise Root CA</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>12</Order>
|
||||
<Path>cmd /c certutil -addstore Root "C:\Deploy\Applications\ZscalerCommercialCertificate-2048-SHA256.crt"</Path>
|
||||
<Description>Install Zscaler Certificate</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Fonts -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>13</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\3OF9.TTF" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy 3OF9 Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>14</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "3 of 9 Barcode" /t REG_SZ /d "3OF9.TTF" /f</Path>
|
||||
<Description>Register 3OF9 Barcode Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>15</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\Code39AzaleaNarrow3.ttf" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy Code39 Azalea Narrow Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>16</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Code39AzaleaNarrow3" /t REG_SZ /d "Code39AzaleaNarrow3.ttf" /f</Path>
|
||||
<Description>Register Code39 Azalea Narrow Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>17</Order>
|
||||
<Path>cmd /c copy "C:\Deploy\Applications\extra\fonts\Code39Azalea.ttf" "%WINDIR%\Fonts\" /Y</Path>
|
||||
<Description>Copy Code39 Azalea Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>18</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts" /v "Code39Azalea" /t REG_SZ /d "Code39Azalea.ttf" /f</Path>
|
||||
<Description>Register Code39 Azalea Font</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- OpenText installers -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>19</Order>
|
||||
<Path>cmd /c "C:\Deploy\Applications\extra\opentext\Setup-OpenText.cmd"</Path>
|
||||
<Description>Install OpenText HostExplorer SP1 (shopfloor method)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>20</Order>
|
||||
<Path>C:\Deploy\Applications\extra\opentext\J2SE_Runtime_Environment_1.6.0_22_Static_Config_V2_Co-Exist.EXE /silent /norestart</Path>
|
||||
<Description>Install J2SE Runtime Environment 1.6.0_22</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Adobe -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>22</Order>
|
||||
<Path>msiexec /i "C:\Deploy\Applications\extra\adobe\AcroRead.msi" TRANSFORMS="C:\Deploy\Applications\extra\adobe\AcroRead.mst" /quiet /norestart</Path>
|
||||
<Description>Install Adobe</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>23</Order>
|
||||
<Path>msiexec /p "C:\Deploy\Applications\extra\adobe\AcroRdrDCUpd2500120531.msp" /quiet /norestart</Path>
|
||||
<Description>Apply Adobe Reader Update</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Java JRE 8 Enterprise Installation -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>26</Order>
|
||||
<Path>C:\Deploy\Applications\extra\java\jre-8u441-windows-i586.exe /s INSTALLDIR="C:\Program Files (x86)\Java\jre1.8.0_441" STATIC=1 AUTO_UPDATE=0 REBOOT=0 SPONSORS=0 WEB_JAVA=0 /L "C:\Windows\Logs\java-install.log"</Path>
|
||||
<Description>Install Java JRE 8 Update 441 with Enterprise Settings and Logging</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Updates via Registry (64-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>27</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\JavaSoft\Java Update\Policy" /v EnableJavaUpdate /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Updates (64-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Updates via Registry (32-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>28</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Wow6432Node\JavaSoft\Java Update\Policy" /v EnableJavaUpdate /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Updates (32-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Update Check (64-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>29</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\JavaSoft\Java Update\Policy" /v EnableAutoUpdateCheck /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Update Check (64-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Java Auto Update Check (32-bit) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>30</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Wow6432Node\JavaSoft\Java Update\Policy" /v EnableAutoUpdateCheck /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Java Auto Update Check (32-bit)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Cortana -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>31</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v AllowCortana /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Cortana</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Web Search in Start Menu -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>33</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v DisableWebSearch /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable Web Search in Start Menu</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Bing / web search suggestions in Start menu.
|
||||
BingSearchEnabled is documented at HKCU\Software\Microsoft\Windows\CurrentVersion\Search
|
||||
(not HKLM), so the old HKLM policy placement was a no-op. The current documented
|
||||
suppression is DisableSearchBoxSuggestions=1 at Software\Policies\Microsoft\Windows\Explorer.
|
||||
Writing it into the Default User hive so every newly-created account inherits it. -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>34</Order>
|
||||
<Path>cmd /c reg load HKU\TempDU C:\Users\Default\NTUSER.DAT & reg add "HKU\TempDU\Software\Policies\Microsoft\Windows\Explorer" /v DisableSearchBoxSuggestions /t REG_DWORD /d 1 /f & reg unload HKU\TempDU</Path>
|
||||
<Description>Disable Bing/web search suggestions (Default User hive)</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Search Web when searching Windows -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>35</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v ConnectedSearchUseWeb /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Connected Search Use Web</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Cortana Speech Recognition -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>36</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\InputPersonalization" /v AllowInputPersonalization /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Cortana Speech Recognition</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<!-- Disable Search Highlights (news/trending) -->
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>38</Order>
|
||||
<Path>cmd /c reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\Windows Search" /v EnableDynamicContentInWSB /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable Search Highlights</Description>
|
||||
</RunSynchronousCommand>
|
||||
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>39</Order>
|
||||
<Path>powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\Deploy\Applications\BPRT\Register-BulkEnrollTask-Standard.ps1"</Path>
|
||||
<Description>Add bulk enrollment orchestrator to Run key</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>40</Order>
|
||||
<Path>reg.exe load HKU\TempDefault C:\Users\Default\NTUSER.DAT</Path>
|
||||
<Description>Load Default User registry hive</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>41</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable startup delay for all new users</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>42</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v WaitForIdleState /t REG_DWORD /d 0 /f</Path>
|
||||
<Description>Disable wait for idle state for all new users</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>43</Order>
|
||||
<Path>reg.exe add "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced" /v StartShownOnUpgrade /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Prevent Start menu from opening on first login</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>44</Order>
|
||||
<Path>reg.exe unload HKU\TempDefault</Path>
|
||||
<Description>Unload Default User registry hive</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>45</Order>
|
||||
<Path>reg add HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU /v NoAutoUpdate /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable automatic Windows updates (NoAutoUpdate)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>46</Order>
|
||||
<Path>reg add HKLM\Software\Policies\Microsoft\Windows\WindowsUpdate\AU /v AUOptions /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Disable automatic Windows updates (AUOptions)</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>47</Order>
|
||||
<Path>reg.exe add "HKLM\Software\Microsoft\Command Processor" /v AutoRun /t REG_SZ /d "doskey vanilla=powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File C:\Deploy\Applications\BPRT\Show-VanillaConfirmation.ps1" /f</Path>
|
||||
<Description>Define vanilla CMD alias for manual OOBE fallback via Shift+F10</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>48</Order>
|
||||
<Path>reg.exe add "HKLM\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices" /v Deny_All /t REG_DWORD /d 1 /f</Path>
|
||||
<Description>Block all removable media reads to prevent PPKG auto-detection at OOBE</Description>
|
||||
</RunSynchronousCommand>
|
||||
<RunSynchronousCommand wcm:action="add">
|
||||
<Order>49</Order>
|
||||
<Path>powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-standard -StageIndex 3 -Status succeeded -Stage "Ready at OOBE - hand to the employee"</Path>
|
||||
<Description>PXE status: stage 3 of 3, done (the rest needs the employee to log in)</Description>
|
||||
</RunSynchronousCommand>
|
||||
</RunSynchronous>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
<!-- 4. oobeSystem: hide OEM/EULA screens -->
|
||||
<settings pass="oobeSystem">
|
||||
<component name="Microsoft-Windows-Shell-Setup"
|
||||
processorArchitecture="amd64"
|
||||
publicKeyToken="31bf3856ad364e35"
|
||||
language="neutral"
|
||||
versionScope="nonSxS">
|
||||
<OOBE>
|
||||
<HideEULAPage>true</HideEULAPage>
|
||||
<HideOEMRegistrationScreen>true</HideOEMRegistrationScreen>
|
||||
<HideOnlineAccountScreens>false</HideOnlineAccountScreens>
|
||||
<HideWirelessSetupInOOBE>true</HideWirelessSetupInOOBE>
|
||||
<HideLocalAccountScreen>true</HideLocalAccountScreen>
|
||||
<NetworkLocation>Work</NetworkLocation>
|
||||
<ProtectYourPC>3</ProtectYourPC>
|
||||
<SkipUserOOBE>false</SkipUserOOBE>
|
||||
<SkipMachineOOBE>false</SkipMachineOOBE>
|
||||
</OOBE>
|
||||
<FirstLogonCommands>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-standard -StageIndex 4 -Stage "First logon"</CommandLine>
|
||||
<Description>PXE status: stage 4 of 5</Description>
|
||||
<Order>1</Order>
|
||||
</SynchronousCommand>
|
||||
|
||||
|
||||
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>cmd /c sc config wuauserv start= disabled</CommandLine>
|
||||
<Description>disable windows update</Description>
|
||||
<Order>4</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>cmd /c sc stop wuauserv</CommandLine>
|
||||
<Description>disable windows update</Description>
|
||||
<Order>5</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe load HKU\TempDefault C:\Users\Default\NTUSER.DAT</CommandLine>
|
||||
<Description>Load Default User registry hive to reverse startup delay</Description>
|
||||
<Order>6</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v StartupDelayInMSec /f</CommandLine>
|
||||
<Description>Remove startup delay override from Default User</Description>
|
||||
<Order>7</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKU\TempDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Serialize" /v WaitForIdleState /f</CommandLine>
|
||||
<Description>Remove wait for idle state override from Default User</Description>
|
||||
<Order>8</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe unload HKU\TempDefault</CommandLine>
|
||||
<Description>Unload Default User registry hive</Description>
|
||||
<Order>9</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>reg.exe delete "HKLM\SOFTWARE\Policies\Microsoft\Windows\RemovableStorageDevices" /v Deny_All /f</CommandLine>
|
||||
<Description>Restore removable media access after OOBE PPKG block</Description>
|
||||
<Order>10</Order>
|
||||
</SynchronousCommand>
|
||||
<SynchronousCommand wcm:action="add">
|
||||
<CommandLine>powershell.exe -NoP -EP Bypass -File C:\ProgramData\PxeStatus\Send-ImagingStage.ps1 -PCType gea-standard -StageIndex 5 -Stage "Setup complete - enrollment continues on the PC" -Status succeeded</CommandLine>
|
||||
<Description>PXE status: stage 5 of 5</Description>
|
||||
<Order>11</Order>
|
||||
</SynchronousCommand>
|
||||
</FirstLogonCommands>
|
||||
</component>
|
||||
</settings>
|
||||
|
||||
</unattend>
|
||||
|
||||
@@ -7,35 +7,46 @@
|
||||
# NIC driver set), download Blancco rootfs (squashfs), overlay-mount, and
|
||||
# switch_root into Blancco's userspace.
|
||||
#
|
||||
# Verbose trace + shell-on-NIC-failure because silent hangs during Blancco
|
||||
# PXE boot are painful to debug. set -x goes to /dev/console so the screen
|
||||
# shows every step; if no NIC appears after the modprobe sweep, we dump
|
||||
# dmesg / lspci / /proc/modules and drop to sh so the operator can
|
||||
# Quiet by default: one line per step. Add blancco.debug=1 to the kernel
|
||||
# cmdline for the full trace (set -x, modprobe -v, ip link/addr). Failures
|
||||
# always print their diagnostics and drop to sh so the operator can
|
||||
# investigate without re-imaging.
|
||||
|
||||
exec >/dev/console 2>&1
|
||||
set -x
|
||||
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo " Blancco PXE loader (verbose)"
|
||||
echo "============================================"
|
||||
echo ""
|
||||
|
||||
export PATH=/bin:/sbin
|
||||
mount -t proc proc /proc
|
||||
mount -t sysfs sysfs /sys
|
||||
mount -t devtmpfs devtmpfs /dev 2>/dev/null || mount -t tmpfs tmpfs /dev
|
||||
|
||||
DEBUG=""
|
||||
grep -q 'blancco.debug=1' /proc/cmdline && DEBUG=1
|
||||
# dbg CMD...: run only in debug mode
|
||||
dbg() { [ -n "$DEBUG" ] && "$@"; return 0; }
|
||||
if [ -n "$DEBUG" ]; then
|
||||
set -x
|
||||
else
|
||||
printf '\033c' # clear the kernel messages off the screen
|
||||
dmesg -n 1 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Blancco Drive Eraser - loading from $( [ -n "$DEBUG" ] && echo "PXE (debug)" || echo "PXE")"
|
||||
echo ""
|
||||
mkdir -p /tmp /run /run/lower /run/upper /run/work /run/newroot
|
||||
KVER=$(uname -r)
|
||||
echo "Running kernel: $KVER"
|
||||
ls /lib/modules/ 2>/dev/null
|
||||
dbg echo "Running kernel: $KVER"
|
||||
dbg ls /lib/modules/
|
||||
|
||||
echo "[1/5] Loading NIC drivers via modprobe (resolves deps automatically)..."
|
||||
echo " [1/5] Network drivers"
|
||||
# Throw the full common-NIC driver list at the wall. modprobe resolves the
|
||||
# deps from /lib/modules/$KVER/modules.dep (built by prepare-boot-tools).
|
||||
# Anything missing is silently ignored; whatever matches PCI IDs will bind.
|
||||
# USB host controllers first: xhci_pci is a module in the Ubuntu kernel
|
||||
# (CONFIG_USB_XHCI_PCI=m) and there is no udev here, so without this no
|
||||
# USB 3 / USB-C port works and USB-C to RJ45 adapters never appear.
|
||||
for drv in \
|
||||
xhci_pci xhci_pci_renesas \
|
||||
mii libphy ptp \
|
||||
e1000 e1000e igb igc ixgbe ixgbevf i40e ice iavf \
|
||||
tg3 bnx2 bnx2x bnxt_en b44 \
|
||||
@@ -46,15 +57,19 @@ for drv in \
|
||||
alx atl1c atl1e atl2 \
|
||||
via_rhine via_velocity forcedeth \
|
||||
pegasus dm9601 asix ax88179_178a cdc_ether cdc_ncm rndis_host; do
|
||||
modprobe -v "$drv" 2>/dev/null && echo " OK $drv" || true
|
||||
if [ -n "$DEBUG" ]; then
|
||||
modprobe -v "$drv" 2>/dev/null && echo " OK $drv" || true
|
||||
else
|
||||
modprobe "$drv" >/dev/null 2>&1 || true
|
||||
fi
|
||||
done
|
||||
sleep 3
|
||||
|
||||
echo "[2/5] /sys/class/net after driver load:"
|
||||
ls /sys/class/net/ || true
|
||||
ip link || true
|
||||
echo " [2/5] Waiting for a network link"
|
||||
dbg ls /sys/class/net/
|
||||
dbg ip link
|
||||
|
||||
echo " Waiting up to 60s for non-lo interface..."
|
||||
dbg echo " Waiting up to 60s for non-lo interface..."
|
||||
IFACE=""
|
||||
for i in $(seq 1 60); do
|
||||
for n in /sys/class/net/*; do
|
||||
@@ -76,21 +91,87 @@ if [ -z "$IFACE" ]; then
|
||||
exec /bin/sh
|
||||
fi
|
||||
|
||||
echo " IFACE=$IFACE, bringing up..."
|
||||
# The first interface to appear is not necessarily the cabled one: a
|
||||
# second onboard port, a dock or a USB NIC can come first, and a static
|
||||
# IP on a dead port leaves wget at "Connecting" forever. Bring every
|
||||
# wired interface up and use the first with carrier.
|
||||
for n in /sys/class/net/*; do
|
||||
name="${n##*/}"
|
||||
case "$name" in lo|wl*) continue ;; esac
|
||||
ip link set "$name" up 2>/dev/null || ifconfig "$name" up 2>/dev/null || true
|
||||
done
|
||||
dbg echo " Waiting up to 30s for a cabled interface..."
|
||||
CARRIER=""
|
||||
for i in $(seq 1 30); do
|
||||
for n in /sys/class/net/*; do
|
||||
name="${n##*/}"
|
||||
case "$name" in lo|wl*) continue ;; esac
|
||||
# USB NICs enumerate seconds after onboard ports: bring up late ones too
|
||||
[ "$(cat "$n/operstate" 2>/dev/null)" = "down" ] && { ip link set "$name" up 2>/dev/null || ifconfig "$name" up 2>/dev/null || true; }
|
||||
[ "$(cat "$n/carrier" 2>/dev/null)" = "1" ] && CARRIER="$name" && break 2
|
||||
done
|
||||
sleep 1
|
||||
echo -n "."
|
||||
done
|
||||
echo ""
|
||||
if [ -z "$CARRIER" ]; then
|
||||
echo "ERROR: no wired interface has link. Interfaces seen:"
|
||||
for n in /sys/class/net/*; do
|
||||
drv=$(readlink "$n/device/driver" 2>/dev/null)
|
||||
echo " ${n##*/} carrier=$(cat "$n/carrier" 2>/dev/null) driver=${drv##*/}"
|
||||
done
|
||||
echo "=== kernel messages (NIC / firmware / link) ==="
|
||||
dmesg | grep -i -E 'eth|enp|enx|r8152|r8169|e1000e|igc|cdc|firmware|link' | tail -20
|
||||
echo "Check the cable, then type 'exit' to reboot."
|
||||
exec /bin/sh
|
||||
fi
|
||||
IFACE="$CARRIER"
|
||||
|
||||
echo " using $IFACE"
|
||||
ip link set "$IFACE" up || ifconfig "$IFACE" up
|
||||
sleep 2
|
||||
|
||||
SERVER=172.16.9.1
|
||||
|
||||
# Where the release files (rootfs, preferences, licence container,
|
||||
# config.img) come from. Default /blancco, the active release. The webapp's
|
||||
# test menu entry adds blancco.base=/blancco/staged/<version> to boot a
|
||||
# staged release before it is activated. The kmod tarball, progress
|
||||
# reporter and debug key are not part of a release and stay in /blancco.
|
||||
BASE=/blancco
|
||||
for arg in $(cat /proc/cmdline); do
|
||||
case "$arg" in
|
||||
blancco.base=/blancco/*) BASE="${arg#blancco.base=}" ;;
|
||||
esac
|
||||
done
|
||||
case "$BASE" in
|
||||
*..*|*[!A-Za-z0-9._/-]*) BASE=/blancco ;;
|
||||
esac
|
||||
BASE="${BASE%/}"
|
||||
[ "$BASE" = /blancco ] || echo " release under test: $BASE"
|
||||
|
||||
# Coarse progress for the webapp's /imaging dashboard (same endpoint the
|
||||
# WinPE imaging clients use). Stage 1 also resets a finished tile for this
|
||||
# serial. Best effort: a webapp outage must never stop a wipe.
|
||||
post_status() {
|
||||
# sed, not tr: the initrd's busybox has no tr link
|
||||
serial=$(sed 's/["\\]//g' /sys/class/dmi/id/product_serial 2>/dev/null)
|
||||
[ -n "$serial" ] || return 0
|
||||
wget -q -T 5 -O /dev/null --header 'Content-Type: application/json' \
|
||||
--post-data "{\"serial\":\"$serial\",\"pctype\":\"blancco\",\"source\":\"blancco\",\"current_stage\":\"$2\",\"stage_index\":$1,\"stage_total\":7,\"status\":\"in_progress\",\"ip\":\"172.16.9.250\"}" \
|
||||
"http://$SERVER:9009/imaging/status" 2>/dev/null || true
|
||||
}
|
||||
ifconfig "$IFACE" 172.16.9.250 netmask 255.255.255.0 up
|
||||
sleep 1
|
||||
echo " IP: 172.16.9.250 SERVER: $SERVER"
|
||||
ip addr
|
||||
dbg echo " IP: 172.16.9.250 SERVER: $SERVER"
|
||||
dbg ip addr
|
||||
|
||||
echo "[3/5] Downloading airootfs.sfs (~756 MB)..."
|
||||
wget -O /tmp/airootfs.sfs http://$SERVER/blancco/arch/x86_64/airootfs.sfs 2>&1
|
||||
echo " [3/5] Downloading Blancco (~850 MB)"
|
||||
post_status 1 "Downloading Blancco"
|
||||
wget -T 30 -O /tmp/airootfs.sfs http://$SERVER$BASE/arch/x86_64/airootfs.sfs 2>&1
|
||||
[ -s /tmp/airootfs.sfs ] || { echo "ERROR: download failed"; exec /bin/sh; }
|
||||
|
||||
echo "[4/5] Mounting rootfs + overlay..."
|
||||
echo " [4/5] Preparing system"
|
||||
modprobe overlay 2>/dev/null || insmod /lib/modules/$KVER/kernel/fs/overlayfs/overlay.ko 2>/dev/null
|
||||
modprobe squashfs 2>/dev/null || insmod /lib/modules/$KVER/kernel/fs/squashfs/squashfs.ko 2>/dev/null
|
||||
modprobe loop 2>/dev/null
|
||||
@@ -100,8 +181,12 @@ mount -t tmpfs -o size=50% tmpfs /run/upper
|
||||
mkdir -p /run/upper/upper /run/upper/work
|
||||
mount -t overlay overlay -o lowerdir=/run/lower,upperdir=/run/upper/upper,workdir=/run/upper/work /run/newroot
|
||||
|
||||
echo "[5/5] Fetching kmod tarball + config..."
|
||||
wget -O /tmp/kmod.tar.gz http://$SERVER/blancco/kmod.tar.gz 2>&1
|
||||
echo " [5/5] Loading drivers and settings"
|
||||
# Modules (+ iwlwifi firmware) for the running kernel. kmod-$KVER.tar.gz
|
||||
# lets several boot kernels share one web root; kmod.tar.gz is the
|
||||
# original single-kernel name, kept as the fallback.
|
||||
wget -q -T 30 -O /tmp/kmod.tar.gz "http://$SERVER/blancco/kmod-$KVER.tar.gz" 2>/dev/null ||
|
||||
wget -q -T 30 -O /tmp/kmod.tar.gz http://$SERVER/blancco/kmod.tar.gz 2>&1
|
||||
[ -s /tmp/kmod.tar.gz ] && (cd /run/newroot && gunzip -c /tmp/kmod.tar.gz | tar xf - && rm -f /tmp/kmod.tar.gz)
|
||||
|
||||
mkdir -p /run/newroot/albus
|
||||
@@ -111,8 +196,8 @@ mkdir -p /run/newroot/albus
|
||||
# /albus/preferences.save (factory defaults, empty network_share) so
|
||||
# erasure reports never reach SMB. Fail loud here instead.
|
||||
PREFS=/run/newroot/albus/preferences.xml
|
||||
if ! wget -O "$PREFS" "http://$SERVER/blancco/preferences.xml"; then
|
||||
echo "ERROR: preferences.xml download failed from http://$SERVER/blancco/preferences.xml"
|
||||
if ! wget -q -T 30 -O "$PREFS" "http://$SERVER$BASE/preferences.xml"; then
|
||||
echo "ERROR: preferences.xml download failed from http://$SERVER$BASE/preferences.xml"
|
||||
echo "Reports would fall back to factory defaults (no SMB target)."
|
||||
echo "Dropping to shell - check Apache + network."
|
||||
exec /bin/sh
|
||||
@@ -132,11 +217,29 @@ fi
|
||||
# fires, we want the fallback to still have the right SMB target.
|
||||
cp -f "$PREFS" /run/newroot/albus/preferences.save
|
||||
|
||||
if ! wget -O /run/newroot/albus/config.xml "http://$SERVER/blancco/config-clean.xml"; then
|
||||
if ! wget -q -T 30 -O /run/newroot/albus/config.xml "http://$SERVER$BASE/config-clean.xml"; then
|
||||
echo "ERROR: config.xml (license container) download failed."
|
||||
exec /bin/sh
|
||||
fi
|
||||
|
||||
# On a native boot config.img is an initrd, its files land in the
|
||||
# initramfs root and archiso keeps them in /run/initramfs, where Blancco's
|
||||
# /opt/scripts/inc/* pick up report.key (report signing), certificate.ca,
|
||||
# hybrid.ids and flexible_container. This path never had them, so
|
||||
# 1_reportKey wrote an empty /albus/report.key. Recreate the layout on a
|
||||
# tmpfs at the new root's /run; systemd leaves an existing /run mounted.
|
||||
mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run/newroot/run
|
||||
mkdir -p /run/newroot/run/initramfs
|
||||
if wget -q -T 30 -O /tmp/config.img "http://$SERVER$BASE/config.img" &&
|
||||
(cd /run/newroot/run/initramfs && cpio -id < /tmp/config.img 2>/dev/null) &&
|
||||
[ -s /run/newroot/run/initramfs/report.key ]; then
|
||||
dbg ls -la /run/newroot/run/initramfs
|
||||
else
|
||||
echo "ERROR: config.img (report key, certificate) download or unpack failed."
|
||||
exec /bin/sh
|
||||
fi
|
||||
rm -f /tmp/config.img
|
||||
|
||||
mkdir -p /run/newroot/etc/X11/xorg.conf.d
|
||||
# Don't pin a single Xorg driver - hardware varies per site (Intel iGPU,
|
||||
# NVIDIA GK208, AMD, etc). Let Xorg auto-pick. With nomodeset + KMS
|
||||
@@ -168,6 +271,24 @@ HandleLidSwitchDocked=ignore
|
||||
HandlePowerKey=ignore
|
||||
XEOF
|
||||
|
||||
# Restart after an erasure stalled on "A stop job is running for
|
||||
# /mnt/share": the network goes down before the SMB report share is
|
||||
# unmounted, and the CIFS umount waits out the 90s timeout. A mount unit's
|
||||
# timeout is TimeoutSec= (default: the START timeout), so
|
||||
# DefaultTimeoutStopSec alone does not reach it. Lazy + forced umount
|
||||
# detaches the dead share at once; reports are written long before this.
|
||||
mkdir -p /run/newroot/etc/systemd/system.conf.d /run/newroot/etc/systemd/system/mnt-share.mount.d
|
||||
cat > /run/newroot/etc/systemd/system.conf.d/10-fast-stop.conf << XEOF
|
||||
[Manager]
|
||||
DefaultTimeoutStopSec=10s
|
||||
XEOF
|
||||
cat > /run/newroot/etc/systemd/system/mnt-share.mount.d/10-fast-umount.conf << XEOF
|
||||
[Mount]
|
||||
TimeoutSec=10s
|
||||
LazyUnmount=yes
|
||||
ForceUnmount=yes
|
||||
XEOF
|
||||
|
||||
# Disable Xorg screensaver + DPMS so display stays on during long erasures.
|
||||
# Numbered 00-* to run before vendor xinitrc.d hooks.
|
||||
mkdir -p /run/newroot/etc/X11/xinit/xinitrc.d
|
||||
@@ -180,6 +301,115 @@ XEOF
|
||||
chmod +x /run/newroot/etc/X11/xinit/xinitrc.d/00-no-screen-blank.sh
|
||||
|
||||
mkdir -p /run/newroot/proc /run/newroot/sys /run/newroot/dev /run/newroot/run /run/newroot/tmp
|
||||
# Auto-save reports to the SMB share. process=auto never writes the share
|
||||
# (Albus forces auto_backup off outside workflow/manual) but does keep each
|
||||
# report in /reports; a small service copies them across.
|
||||
mkdir -p /run/newroot/usr/local/sbin /run/newroot/etc/systemd/system/multi-user.target.wants
|
||||
cat > /run/newroot/usr/local/sbin/blancco-report-sync << 'XEOF'
|
||||
#!/bin/bash
|
||||
# Copy Blancco reports to the SMB report share.
|
||||
#
|
||||
# In process=auto Albus saves each report to /reports/<document_id>.xml
|
||||
# (and .pdf when pdf+xml is selected) and uploads it to the cloud, but never
|
||||
# writes the share: auto_backup is forced off outside workflow/manual mode.
|
||||
# Albus has the share mounted at /mnt/share already, so copy each finished
|
||||
# file there, named the way a manual Save names it (UTC
|
||||
# YYYYMMDD_HHMMSS_report.<ext>, the time taken from the XML so the PDF of
|
||||
# the same report gets the same name).
|
||||
# Usage: blancco-report-sync [--once]
|
||||
DONE=/var/lib/blancco-report-sync
|
||||
mkdir -p "$DONE"
|
||||
while :; do
|
||||
for f in /reports/*.xml /reports/*.pdf; do
|
||||
[ -s "$f" ] || continue
|
||||
base=$(basename "$f")
|
||||
[ -e "$DONE/$base" ] && continue
|
||||
# Leave files Albus may still be writing.
|
||||
[ $(( $(date +%s) - $(stat -c %Y "$f") )) -ge 5 ] || continue
|
||||
mountpoint -q /mnt/share || continue
|
||||
id=${base%.*}
|
||||
ext=${base##*.}
|
||||
ref="/reports/$id.xml"
|
||||
[ -f "$ref" ] || ref="$f"
|
||||
name="$(date -u -r "$ref" +%Y%m%d_%H%M%S)_report.$ext"
|
||||
if cp "$f" "/mnt/share/$name.part" && mv -f "/mnt/share/$name.part" "/mnt/share/$name"; then
|
||||
touch "$DONE/$base"
|
||||
echo "copied $base to $name"
|
||||
serial=$(tr -d '"\\' 2>/dev/null < /sys/class/dmi/id/product_serial)
|
||||
[ -n "$serial" ] && curl -s -m 5 -o /dev/null -H 'Content-Type: application/json' \
|
||||
-d "{\"serial\":\"$serial\",\"log_lines\":[\"Report copied to share: $name\"]}" \
|
||||
http://172.16.9.1:9009/imaging/status || true
|
||||
else
|
||||
rm -f "/mnt/share/$name.part"
|
||||
echo "copy of $base failed, will retry"
|
||||
fi
|
||||
done
|
||||
[ "$1" = "--once" ] && exit 0
|
||||
sleep 10
|
||||
done
|
||||
XEOF
|
||||
chmod 755 /run/newroot/usr/local/sbin/blancco-report-sync
|
||||
cat > /run/newroot/etc/systemd/system/blancco-report-sync.service << 'XEOF'
|
||||
[Unit]
|
||||
Description=Copy Blancco reports to the SMB report share
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/local/sbin/blancco-report-sync
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
XEOF
|
||||
ln -sf /etc/systemd/system/blancco-report-sync.service \
|
||||
/run/newroot/etc/systemd/system/multi-user.target.wants/blancco-report-sync.service
|
||||
|
||||
# Live erase progress for the /imaging dashboard: follows the AlbusSeven
|
||||
# journal and posts stages and drive percentages. Fetched at boot so the
|
||||
# server copy (playbook/blancco-progress.py) is the only one.
|
||||
if wget -q -T 10 -O /run/newroot/usr/local/sbin/blancco-progress "http://$SERVER/blancco/blancco-progress.py"; then
|
||||
chmod 755 /run/newroot/usr/local/sbin/blancco-progress
|
||||
cat > /run/newroot/etc/systemd/system/blancco-progress.service << 'XEOF'
|
||||
[Unit]
|
||||
Description=Report Blancco erase progress to the PXE webapp
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/python3 /usr/local/sbin/blancco-progress
|
||||
Restart=on-failure
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
XEOF
|
||||
ln -sf /etc/systemd/system/blancco-progress.service \
|
||||
/run/newroot/etc/systemd/system/multi-user.target.wants/blancco-progress.service
|
||||
else
|
||||
echo " (no blancco-progress.py on server: no live progress on the dashboard)"
|
||||
fi
|
||||
|
||||
# Site timezone for Blancco's own clock (GUI, logs). Report timestamps keep
|
||||
# an explicit UTC offset; report.utc_offset in the preferences stays 0
|
||||
# because it is a fixed offset and cannot follow daylight saving.
|
||||
if [ -e /run/newroot/usr/share/zoneinfo/America/New_York ]; then
|
||||
ln -sf /usr/share/zoneinfo/America/New_York /run/newroot/etc/localtime
|
||||
fi
|
||||
|
||||
# Debug boots only: Blancco's own debug mode (/etc/debug.environment makes
|
||||
# isDebugMode true, so 2_sshd starts sshd) plus an SSH key for root.
|
||||
# Root is locked and sshd is key-only, so without the key there is no way
|
||||
# in. The key is served from the PXE server and never baked in here.
|
||||
if [ -n "$DEBUG" ]; then
|
||||
touch /run/newroot/etc/debug.environment
|
||||
mkdir -p /run/newroot/root/.ssh
|
||||
if wget -q -T 10 -O /run/newroot/root/.ssh/authorized_keys "http://$SERVER/blancco/debug_authorized_keys"; then
|
||||
chmod 700 /run/newroot/root/.ssh
|
||||
chmod 600 /run/newroot/root/.ssh/authorized_keys
|
||||
echo " debug: sshd on, root key installed"
|
||||
else
|
||||
echo " debug: no debug_authorized_keys on server, SSH stays closed"
|
||||
fi
|
||||
fi
|
||||
|
||||
mount --move /proc /run/newroot/proc
|
||||
mount --move /sys /run/newroot/sys
|
||||
mount --move /dev /run/newroot/dev
|
||||
@@ -196,5 +426,6 @@ mount --bind /dev/null /run/newroot/sys/power/disk 2>/dev/null || true
|
||||
mount --bind /dev/null /run/newroot/sys/power/mem_sleep 2>/dev/null || true
|
||||
mount --bind /dev/null /run/newroot/sys/power/autosleep 2>/dev/null || true
|
||||
|
||||
echo "Switching root..."
|
||||
echo " Starting Blancco..."
|
||||
post_status 2 "Starting Blancco"
|
||||
exec switch_root /run/newroot /sbin/init
|
||||
|
||||
@@ -1,49 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- FASDHFOSGHSVASLGHASDLASDFUILEFHLKJDKLFJGKLXCJGHFKLJDGHKLFJ -->
|
||||
<root>
|
||||
<blancco type="albus-preferences" version="4.15"/>
|
||||
<blancco type="albus-preferences" version="4.20"/>
|
||||
<version>
|
||||
<generation>7</generation>
|
||||
<major>15</major>
|
||||
<minor>1</minor>
|
||||
<major>20</major>
|
||||
<minor>0</minor>
|
||||
</version>
|
||||
|
||||
<configuration>
|
||||
|
||||
<device_enrollment_detection>
|
||||
<persistent_software>false</persistent_software>
|
||||
</device_enrollment_detection>
|
||||
|
||||
<integrity>qJ4hjOqd1NaMbEPsbmaz+8gZlG1duEXinTXI+Wrsc00=</integrity>
|
||||
<configuration dect="3.15.0" imported="false">
|
||||
<processes type="array" key="process">
|
||||
<process>workflow</process>
|
||||
<process>auto</process>
|
||||
<process selected="true">auto</process>
|
||||
<process>semi</process>
|
||||
<process selected="true">manual</process>
|
||||
<process>manual</process>
|
||||
</processes>
|
||||
|
||||
<hardware_tests type="array" key="test" enabled="false">
|
||||
<test enabled="true" mandatory="false" auto="true" threshold="60">battery</test>
|
||||
<hardware_tests type="array" key="test" enabled="true">
|
||||
<test enabled="false" mandatory="false" auto="true" threshold="60">battery</test>
|
||||
<test enabled="true" mandatory="false" auto="true">cpu</test>
|
||||
<test enabled="false" mandatory="false" auto="true" passes="0">memory</test>
|
||||
<test enabled="true" mandatory="false" auto="true">motherboard</test>
|
||||
<test enabled="true" mandatory="false" auto="false" threshold="50" duration="600">battery_discharge</test>
|
||||
<test enabled="true" mandatory="false" auto="false">bios_logo</test>
|
||||
<test enabled="true" mandatory="false" auto="false" threshold="80" duration="180">cpu_stress</test>
|
||||
<test enabled="true" mandatory="false" auto="false">display</test>
|
||||
<test enabled="true" mandatory="false" auto="false" formfactor="compact" language="us">keyboard</test>
|
||||
<test enabled="true" mandatory="false" auto="false">microphone</test>
|
||||
<test enabled="true" mandatory="false" auto="false" targets="">network</test>
|
||||
<test enabled="true" mandatory="false" auto="false" tests="R">optical_devices</test>
|
||||
<test enabled="true" mandatory="false" auto="false">sound</test>
|
||||
<test enabled="true" mandatory="false" auto="false">pointing_devices</test>
|
||||
<test enabled="true" mandatory="false" auto="false">sim_presence</test>
|
||||
<test enabled="true" mandatory="false" auto="false">speaker</test>
|
||||
<test enabled="true" mandatory="false" auto="false">touchscreen</test>
|
||||
<test enabled="true" mandatory="false" auto="false" tests="R">usb_ports</test>
|
||||
<test enabled="true" mandatory="false" auto="false">webcam</test>
|
||||
<test enabled="true" mandatory="false" auto="false">wifi</test>
|
||||
<test enabled="false" mandatory="false" auto="false" threshold="50" duration="600">battery_discharge</test>
|
||||
<test enabled="false" mandatory="false" auto="false">bios_logo</test>
|
||||
<test enabled="false" mandatory="false" auto="false" threshold="80" duration="180">cpu_stress</test>
|
||||
<test enabled="false" mandatory="false" auto="false">display</test>
|
||||
<test enabled="false" mandatory="false" auto="false" formfactor="compact" language="us">keyboard</test>
|
||||
<test enabled="false" mandatory="false" auto="false">microphone</test>
|
||||
<test enabled="false" mandatory="false" auto="false" targets="">network</test>
|
||||
<test enabled="false" mandatory="false" auto="false" tests="R">optical_devices</test>
|
||||
<test enabled="false" mandatory="false" auto="false">sound</test>
|
||||
<test enabled="false" mandatory="false" auto="false">pointing_devices</test>
|
||||
<test enabled="false" mandatory="false" auto="false">sim_presence</test>
|
||||
<test enabled="false" mandatory="false" auto="false">speaker</test>
|
||||
<test enabled="false" mandatory="false" auto="false">touchscreen</test>
|
||||
<test enabled="false" mandatory="false" auto="false" tests="R">usb_ports</test>
|
||||
<test enabled="false" mandatory="false" auto="false">webcam</test>
|
||||
<test enabled="false" mandatory="false" auto="false">wifi</test>
|
||||
</hardware_tests>
|
||||
|
||||
<layouts type="array" key="layout">
|
||||
<layout selected="true">us</layout>
|
||||
<layout>be</layout>
|
||||
@@ -71,7 +64,6 @@
|
||||
<layout>se</layout>
|
||||
<layout>sk</layout>
|
||||
</layouts>
|
||||
|
||||
<locales type="array" key="locale">
|
||||
<locale selected="true">en_US</locale>
|
||||
<locale>de_DE</locale>
|
||||
@@ -88,119 +80,110 @@
|
||||
<locale>zh_CN</locale>
|
||||
<locale>zh_TW</locale>
|
||||
</locales>
|
||||
|
||||
<erasure_standards key="standard" type="array">
|
||||
<erasure_standards type="array" key="standard">
|
||||
<standard enabled="true" id="afssi_5020" name="Air Force System Security Instruction 5020"/>
|
||||
<standard enabled="true" id="aperiodic_random" name="Aperiodic random overwrite"/>
|
||||
<standard enabled="true" id="bl_ssd" name="Blancco SSD Erasure"/>
|
||||
<standard enabled="true" id="bruce_schneier" name="Bruce Schneier's Algorithm"/>
|
||||
<standard enabled="true" id="bsi_vs" name="BSI-2011-VS"/>
|
||||
<standard enabled="true" id="bsi_gs" name="BSI-GS"/>
|
||||
<standard enabled="true" id="bsi_gse" name="BSI-GSE"/>
|
||||
<standard enabled="true" id="bsi_gsk" name="BSI-GSK"/>
|
||||
<standard enabled="true" id="bl_ssd" name="Blancco SSD Erasure"/>
|
||||
<standard enabled="true" id="bruce_schneier" name="Bruce Schneier's Algorithm"/>
|
||||
<standard enabled="true" id="cesg_higher" name="CESG CPA - Higher Level"/>
|
||||
<standard enabled="true" id="crypto_erase" name="Sanitize Cryptographic Erasure"/>
|
||||
<standard enabled="true" id="dod" name="DoD 5220.22-M"/>
|
||||
<standard enabled="true" id="dod_ece" name="DoD 5220.22-M ECE"/>
|
||||
<standard enabled="true" id="nist_clear" name="NIST 800-88 Clear" pattern="0x00" pattern_type="static"/>
|
||||
<standard selected="true" enabled="true" id="nist_purge" name="NIST 800-88 Purge"/>
|
||||
<standard enabled="true" id="firmware" name="Firmware Based Erasure"/>
|
||||
<standard enabled="true" id="extended_firmware" name="Extended Firmware Based Erasure"/>
|
||||
<standard enabled="true" id="hmg_lower" name="HMG Infosec Standard 5, Lower Standard"/>
|
||||
<standard enabled="true" id="firmware" name="Firmware Based Erasure"/>
|
||||
<standard enabled="true" id="hmg_higher" name="HMG Infosec Standard 5, Higher Standard"/>
|
||||
<standard enabled="true" id="nsa" name="NSA 130-1"/>
|
||||
<standard enabled="true" id="ncsc" name="National Computer Security Center (NCSC-TG-025)"/>
|
||||
<standard enabled="true" id="navso" name="Navy Staff Office Publication (NAVSO P-5239-26)"/>
|
||||
<standard enabled="true" id="tcg_crypto_erase" name="TCG Cryptographic Erasure"/>
|
||||
<standard enabled="true" id="opnavinst" name="OPNAVINST 5239.1A"/>
|
||||
<standard enabled="true" id="peter_gutmann" name="Peter Gutmann's Algorithm"/>
|
||||
<standard enabled="true" id="random_byte_3x" name="Random Byte Overwrite (3x)"/>
|
||||
<standard enabled="true" id="rcmp_tssit" name="RCMP TSSIT OPS-II"/>
|
||||
<standard enabled="true" id="us_army" name="U.S. Army AR380-19"/>
|
||||
<standard enabled="true" id="hmg_lower" name="HMG Infosec Standard 5, Lower Standard"/>
|
||||
<standard enabled="true" id="ieee_clear" name="IEEE 2883-2022 Clear"/>
|
||||
<standard enabled="true" id="ieee_purge" name="IEEE 2883-2022 Purge"/>
|
||||
<standard enabled="false" id="nist_pc" name="NIST 800-88" pattern="0x00" pattern_type="static"/>
|
||||
<standard enabled="true" id="nist_clear" name="NIST 800-88 Clear" pattern="0x00" pattern_type="static"/>
|
||||
<standard selected="true" enabled="true" id="nist_purge" name="NIST 800-88 Purge"/>
|
||||
<standard enabled="true" id="nist_clear_rev2" name="NIST 800-88r2 Clear" pattern="0x00" pattern_type="static"/>
|
||||
<standard enabled="true" id="nist_purge_rev2" name="NIST 800-88r2 Purge"/>
|
||||
<standard enabled="true" id="nsa" name="NSA 130-1"/>
|
||||
<standard enabled="true" id="ncsc" name="National Computer Security Center (NCSC-TG-025)"/>
|
||||
<standard enabled="true" id="navso" name="Navy Staff Office Publication (NAVSO P-5239-26)"/>
|
||||
<standard enabled="true" id="opnavinst" name="OPNAVINST 5239.1A"/>
|
||||
<standard enabled="true" id="peter_gutmann" name="Peter Gutmann's Algorithm"/>
|
||||
<standard enabled="true" id="rcmp_tssit" name="RCMP TSSIT OPS-II"/>
|
||||
<standard enabled="true" id="random_byte_3x" name="Random Byte Overwrite (3x)"/>
|
||||
<standard enabled="true" id="crypto_erase" name="Sanitize Cryptographic Erasure"/>
|
||||
<standard enabled="true" id="tcg_crypto_erase" name="TCG Cryptographic Erasure"/>
|
||||
<standard enabled="true" id="us_army" name="U.S. Army AR380-19"/>
|
||||
</erasure_standards>
|
||||
|
||||
<verification_standards key="standard" type="array">
|
||||
<standard enabled="true" id="all_bytes_same" pattern="0x00" name="All bytes the same"/>
|
||||
<verification_standards type="array" key="standard">
|
||||
<standard enabled="true" id="all_bytes_same" name="All bytes the same" pattern="0x00"/>
|
||||
<standard enabled="true" id="all_ones" name="All ones"/>
|
||||
<standard enabled="true" id="all_sectors_same" selected="true" name="All sectors the same"/>
|
||||
<standard selected="true" enabled="true" id="all_sectors_same" name="All sectors the same"/>
|
||||
<standard enabled="true" id="all_zeros" name="All zeros"/>
|
||||
</verification_standards>
|
||||
|
||||
<adapters key="adapter" type="array"/>
|
||||
|
||||
<network>
|
||||
<enabled>false</enabled>
|
||||
<enabled>true</enabled>
|
||||
<dhcp>true</dhcp>
|
||||
<sshd>false</sshd>
|
||||
<looping>false</looping>
|
||||
<address/>
|
||||
<mask/>
|
||||
<gateway/>
|
||||
<dns1/>
|
||||
<dns2/>
|
||||
<vlan/>
|
||||
<address></address>
|
||||
<mask></mask>
|
||||
<gateway></gateway>
|
||||
<dns1></dns1>
|
||||
<dns2></dns2>
|
||||
<vlan></vlan>
|
||||
</network>
|
||||
|
||||
<network_security>
|
||||
<enabled>false</enabled>
|
||||
<fallback>false</fallback>
|
||||
<protocols key="protocol" type="array">
|
||||
<certificate>false</certificate>
|
||||
<protocols type="array" key="protocol">
|
||||
<protocol selected="true">peap</protocol>
|
||||
<protocol>tls</protocol>
|
||||
</protocols>
|
||||
<certificate>false</certificate>
|
||||
<identity encrypted="false"/>
|
||||
<password encrypted="false"/>
|
||||
<identity encrypted="false"></identity>
|
||||
<password encrypted="false"></password>
|
||||
</network_security>
|
||||
|
||||
<proxy>
|
||||
<proxy_address encrypted="false"/>
|
||||
<proxy_port encrypted="false"/>
|
||||
<proxy_username encrypted="false"/>
|
||||
<proxy_password encrypted="false"/>
|
||||
</proxy>
|
||||
|
||||
<mc>
|
||||
<username encrypted="false"/>
|
||||
<password encrypted="false"/>
|
||||
<hostname/>
|
||||
<port/>
|
||||
<timeout>20</timeout>
|
||||
<validate_remote_certificate>true</validate_remote_certificate>
|
||||
</mc>
|
||||
|
||||
<network_share>
|
||||
<username encrypted="false">blancco</username>
|
||||
<password encrypted="false">blancco</password>
|
||||
<domain/>
|
||||
<domain></domain>
|
||||
<hostname>172.16.9.1</hostname>
|
||||
<path>blancco-reports</path>
|
||||
<protocols key="protocol" type="array">
|
||||
<protocols type="array" key="protocol">
|
||||
<protocol selected="true">smb</protocol>
|
||||
</protocols>
|
||||
</network_share>
|
||||
|
||||
<workflow default="false" offline="false"/>
|
||||
|
||||
<proxy>
|
||||
<proxy_address encrypted="false"></proxy_address>
|
||||
<proxy_port encrypted="false"></proxy_port>
|
||||
<proxy_username encrypted="false"></proxy_username>
|
||||
<proxy_password encrypted="false"></proxy_password>
|
||||
</proxy>
|
||||
<mc>
|
||||
<username encrypted="true">k7vMT3SelitU7wIfPSTGZiZa/mNcMlwpPsxGNaBDc2Y1s7VsOBHXEsopfRupMP+shwmLq/sKDIgGR3v9hGrN5A1gmDvvMw/Z+ClZmGIeqhzomR34yUncQA/6GXFq6jc4CNtwXQwKRAvw+stQpE9RYqNvtgFyfmQufOrz75xQyejLXUDmA7KpS6ycdXQxzFKUoqRJOEGlM6I2efjEKkjgKQrvkj8nuRDHw5g5hYXyapKXJhJn7fbwK+AWN+vKx4cceSWVM0wqvofq5BnYj+7hyD5RHphoUI1NqDYnhj2qj8dt3nGKi6EpaXtGJTnXmXplOxbT/PLtZ2afYkxZWRH96negSweOJ4fYkRcJQONe8wGukufSLh/pQcY3NNwkJv7UNh2tC4ogyEKo+E8+n3beImrUWybl6FlUk2lnyQYJFihnnolepze4w5JmPZ2yTEbRi94OUj+bdNEt4ZqRBK2HuDfCI/jzz7h+ytyA2H2+SZWQGjr/oy/RN15a6sgzS5T7Nys+yAbXAH8HSMQm5KUEtLWSSICUpNbRsBOcC+oQhoJnCzWNafj0w5gY/uTNjBm9F0Yd+kjdcO2kqnBF6aeFaLWX6HkTh1DCsMyIJtOr5wZYUmLLtRwgsruhauSbkkQxJA5iy9QsC2Kv+SSACeTLU4WP99Q5MEQIWb+Tjx/S4z6qISgMhxhu4ioteopClaBSizn2oBd/y6gdp7Fz0FK8JSOmPx+pwT3QrDULoK07O5w=</username>
|
||||
<password encrypted="true">eHTwqUlvjRsTYm5ljXTb8uN4hiG6ceE5OIBO0PA+TSMCwmI9qD6co8tmDHCwRdiDdZH2bssDAW40ZPQNfxTdKbDHZ0hunEcQob0xnrr8668L4OkZlWx5V17k8KXB5QgBUd0v1ALnvp9m2SQbsnP6IljTtIL+cZNnfrpmSrvPoGFe8fuoxwMrRE9PZett0DHuYnsnaaP7lD/hAWy8hONsp6MbegS0YA7vZi56cFSrpFnPwjWbNxYREfO3FY6kxxOZoslGARLKLS9//EA0DmOzN4HcXhKhVsiIIbNS1rUEpZi8PsAjS9EKPBMaD2riKlmQYkcw4gIis8Cdj649TsqG5+VESRtXtGCelaGPKSZJaeh7x3Obh6Me+TYQBN5gx4YBUB8Ao11MJR1i5cZj0vdYLPknIsZ9EaFOtRozIo5S37sd7uhgsLPX9pUKJBiDMy+a/CP07rqlKS7pI4+onhInRapU1hwTaBQuO1DxvjhtPXwoPrz0aadmq3jXREhs1YRiJ0bIpZcTA6c9zWYWBSQQ/kfbBB18BSEBZmKfUq42K/8ZDmGuhS/rVIXKLU14/VL9vMqngMfxZ9rBLzTQobXmkt9dKTO9VKxd90P3j3epTQXQsPZ2hwW1rF7NlRA3il2iL94aRAgOGWGe0P9O18VmKEd9uGKWVjXid6vNKBK5m5JHBMoN5n7eXxo9uKQEn8Ex6CKN7vMVvbms25RcUiHUOg==</password>
|
||||
<hostname>classic.eu-west-1.blancco.cloud</hostname>
|
||||
<port>443</port>
|
||||
<timeout>20</timeout>
|
||||
<validate_remote_certificate>false</validate_remote_certificate>
|
||||
</mc>
|
||||
<workflow offline="false" default="true"></workflow>
|
||||
<erasure>
|
||||
<drive_self_test type="short">false</drive_self_test>
|
||||
<max_simultaneous_erasures>50</max_simultaneous_erasures>
|
||||
<remove_hidden_areas>false</remove_hidden_areas>
|
||||
<remove_hidden_areas>true</remove_hidden_areas>
|
||||
<preserve_recovery_partition>false</preserve_recovery_partition>
|
||||
<store_erasure_progress>false</store_erasure_progress>
|
||||
<remapped mandatory="false" threshold="0">false</remapped>
|
||||
<remapped mandatory="false" threshold="0">true</remapped>
|
||||
<action_if_not_possible>interrupt</action_if_not_possible>
|
||||
<fail_on_remapped_sectors>false</fail_on_remapped_sectors>
|
||||
<fail_on_erasure_errors threshold="5">true</fail_on_erasure_errors>
|
||||
<fail_on_read_errors threshold="5">true</fail_on_read_errors>
|
||||
<verification>10</verification>
|
||||
<enforced_rules key="rule" type="array">
|
||||
<enforced_rules type="array" key="rule">
|
||||
<rule enabled="false" name="enforced_standard" standard_id="bl_ssd" target="ssd"/>
|
||||
<rule enabled="true" name="fallback_nist_standard" standard_id="nist_purge" fallback_id="nist_clear"/>
|
||||
</enforced_rules>
|
||||
<drive_self_test type="short">false</drive_self_test>
|
||||
<fail_on_failed_self_test>false</fail_on_failed_self_test>
|
||||
<fail_on_low_erasure_speed threshold="1">false</fail_on_low_erasure_speed>
|
||||
<fail_on_timeout threshold="1">false</fail_on_timeout>
|
||||
@@ -219,36 +202,29 @@
|
||||
<allow_tcg_reset_write_pointers>true</allow_tcg_reset_write_pointers>
|
||||
<allow_tcg_unmap>true</allow_tcg_unmap>
|
||||
</erasure>
|
||||
|
||||
<fingerprint>
|
||||
<enabled>false</enabled>
|
||||
<location>67000</location>
|
||||
</fingerprint>
|
||||
|
||||
<bootable_report>
|
||||
<enabled>true</enabled>
|
||||
</bootable_report>
|
||||
|
||||
<format_disk>
|
||||
<enabled>false</enabled>
|
||||
<type>ntfs</type>
|
||||
</format_disk>
|
||||
|
||||
<spin_down_idle_disk>
|
||||
<enabled>false</enabled>
|
||||
</spin_down_idle_disk>
|
||||
|
||||
<raid>
|
||||
<show_logical_disks>false</show_logical_disks>
|
||||
<show_logical_disks>true</show_logical_disks>
|
||||
<enable_passthrough>true</enable_passthrough>
|
||||
<kill_logical_disks>true</kill_logical_disks>
|
||||
<kill_logical_disks>false</kill_logical_disks>
|
||||
</raid>
|
||||
|
||||
<hotplug>
|
||||
<enabled>false</enabled>
|
||||
<timeout>30</timeout>
|
||||
<timeout>60</timeout>
|
||||
</hotplug>
|
||||
|
||||
<external_device_service>
|
||||
<enabled>false</enabled>
|
||||
<port>80</port>
|
||||
@@ -264,28 +240,24 @@
|
||||
<test enabled="true">webcam</test>
|
||||
</hardware_tests>
|
||||
</external_device_service>
|
||||
|
||||
<license>
|
||||
<provider>bios</provider>
|
||||
<provider_report>bios</provider_report>
|
||||
<provider>clurichaun</provider>
|
||||
<provider_report>clurichaun</provider_report>
|
||||
<edition>Enterprise Volume Edition</edition>
|
||||
<offline></offline>
|
||||
<mode>Drive Eraser</mode>
|
||||
<offline/>
|
||||
</license>
|
||||
|
||||
<image>
|
||||
<description/>
|
||||
<creation_date>2025-03-10</creation_date>
|
||||
<description>BMP Internet - June 2026</description>
|
||||
<creation_date>2026-06-22</creation_date>
|
||||
</image>
|
||||
|
||||
<customer_data>
|
||||
<business_location/>
|
||||
<customer_license>General Electric Company</customer_license>
|
||||
<erasure_person/>
|
||||
<erasure_provider/>
|
||||
<business_name/>
|
||||
<business_location></business_location>
|
||||
<customer_license>GE Aerospace</customer_license>
|
||||
<erasure_person></erasure_person>
|
||||
<erasure_provider></erasure_provider>
|
||||
<business_name>Non_GEAE_Wired</business_name>
|
||||
</customer_data>
|
||||
|
||||
<settings>
|
||||
<gui>
|
||||
<erasure_console>local</erasure_console>
|
||||
@@ -295,11 +267,11 @@
|
||||
<ssd_erasure_exception>true</ssd_erasure_exception>
|
||||
<report_view locked="true">standard</report_view>
|
||||
<erasure_settings locked="false"/>
|
||||
<show_customer>false</show_customer>
|
||||
<show_customer>true</show_customer>
|
||||
<show_operator>false</show_operator>
|
||||
<show_partitions>false</show_partitions>
|
||||
<screensaver locked="false">
|
||||
<timeout>30</timeout>
|
||||
<timeout>86400</timeout>
|
||||
<show_exceptions>false</show_exceptions>
|
||||
</screensaver>
|
||||
<hexviewer>
|
||||
@@ -309,7 +281,7 @@
|
||||
<drive_selection_percentage>5</drive_selection_percentage>
|
||||
<reconfigure_controller_mode>
|
||||
<timeout enabled="false">30</timeout>
|
||||
<default_focus_on_proceed>true</default_focus_on_proceed>
|
||||
<default_focus_on_proceed>false</default_focus_on_proceed>
|
||||
</reconfigure_controller_mode>
|
||||
<disable_sid_block_authentication>
|
||||
<timeout enabled="true">0</timeout>
|
||||
@@ -323,8 +295,8 @@
|
||||
<accessibility>
|
||||
<available>false</available>
|
||||
<enabled>true</enabled>
|
||||
<speaking_rate>80</speaking_rate>
|
||||
<key_echo>true</key_echo>
|
||||
<speaking_rate>80</speaking_rate>
|
||||
</accessibility>
|
||||
<sound_level>70</sound_level>
|
||||
<input_method>
|
||||
@@ -334,82 +306,55 @@
|
||||
</input_method>
|
||||
</gui>
|
||||
</settings>
|
||||
|
||||
<custom_fields type="array" key="custom_field"/>
|
||||
<exclude_verification_ranges type="array" key="exclude_verification_range"/>
|
||||
<report>
|
||||
<per_drive>false</per_drive>
|
||||
<auto_backup>true</auto_backup>
|
||||
<extensions type="array" key="extension">
|
||||
<extension selected="true">xml</extension>
|
||||
<extension>xml</extension>
|
||||
<extension selected="true">pdf+xml</extension>
|
||||
</extensions>
|
||||
<utc_offset>0</utc_offset>
|
||||
<network_info>true</network_info>
|
||||
<custom_signature>
|
||||
<enabled>false</enabled>
|
||||
<label/>
|
||||
<label></label>
|
||||
</custom_signature>
|
||||
<drive_life_estimation>false</drive_life_estimation>
|
||||
</report>
|
||||
|
||||
<custom_fields type="array" key="custom_field">
|
||||
<custom_field type="input" mandatory="true" locked="false" per_drive="false" show_in_editor="false" pattern="" hint="">
|
||||
<name>custom_field_1</name>
|
||||
<title>GERITM Number</title>
|
||||
<value/>
|
||||
</custom_field>
|
||||
<custom_field type="input" mandatory="true" locked="false" per_drive="false" show_in_editor="false" pattern="" hint="">
|
||||
<name>custom_field_2</name>
|
||||
<title>Device Name</title>
|
||||
<value/>
|
||||
</custom_field>
|
||||
<custom_field type="input" mandatory="true" locked="false" per_drive="false" show_in_editor="false" pattern="" hint="">
|
||||
<name>custom_field_3</name>
|
||||
<title>Device Serial Number</title>
|
||||
<value/>
|
||||
</custom_field>
|
||||
<custom_field type="input" mandatory="false" locked="true" per_drive="false" show_in_editor="false" pattern="" hint="">
|
||||
<name>custom_field_4</name>
|
||||
<title>Version</title>
|
||||
<value>EVE_20250310</value>
|
||||
</custom_field>
|
||||
</custom_fields>
|
||||
|
||||
<exclude_verification_ranges type="array" key="exclude_verification_range"/>
|
||||
|
||||
<wireless>
|
||||
<enabled>true</enabled>
|
||||
<ssid encrypted="false">INTERNETACCESS</ssid>
|
||||
<password encrypted="false">Acc3ssGr@nted22</password>
|
||||
<ssid encrypted="false"></ssid>
|
||||
<password encrypted="false"></password>
|
||||
<key_mgmt>WPA-PSK</key_mgmt>
|
||||
<scan_ssid>0</scan_ssid>
|
||||
</wireless>
|
||||
|
||||
<cd_eject>
|
||||
<after_boot>true</after_boot>
|
||||
<after_boot>false</after_boot>
|
||||
<after_erasure>false</after_erasure>
|
||||
<after_report>false</after_report>
|
||||
<at_shutdown>false</at_shutdown>
|
||||
</cd_eject>
|
||||
|
||||
<system_shutdown>
|
||||
<after_erasure>false</after_erasure>
|
||||
<after_successful_erasure>false</after_successful_erasure>
|
||||
</system_shutdown>
|
||||
|
||||
<system_restart>
|
||||
<after_erasure>false</after_erasure>
|
||||
<after_successful_erasure>false</after_successful_erasure>
|
||||
</system_restart>
|
||||
|
||||
<device_enrollment_detection>
|
||||
<persistent_software>false</persistent_software>
|
||||
</device_enrollment_detection>
|
||||
<vnc_remote>
|
||||
<enabled>false</enabled>
|
||||
<mode>viewer</mode>
|
||||
<repeater_hostname/>
|
||||
<repeater_hostname></repeater_hostname>
|
||||
<port>5900</port>
|
||||
<password encrypted="false"/>
|
||||
<password encrypted="false"></password>
|
||||
</vnc_remote>
|
||||
|
||||
<nvme_over_fabrics type="array" key="connection" enabled="false"/>
|
||||
|
||||
<nvme_over_fabrics enabled="false" type="array" key="connection"/>
|
||||
<update>
|
||||
<enabled>false</enabled>
|
||||
<intervals type="array" key="interval">
|
||||
@@ -420,6 +365,6 @@
|
||||
</intervals>
|
||||
<url>https://cdn.blancco.com/products/drive_eraser/download/ota/drive_eraser.json</url>
|
||||
</update>
|
||||
<adapters type="array" key="adapter"/>
|
||||
</configuration>
|
||||
</root>
|
||||
<!-- JFLKHGDJLKFHGJCXLKGJFLKDJKLHFELIUFDSALDSAHGLSAVSHGSOFHDSAF -->
|
||||
|
||||
285
playbook/blancco-progress.py
Normal file
285
playbook/blancco-progress.py
Normal file
@@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Report Blancco erase progress to the PXE webapp's /imaging dashboard.
|
||||
|
||||
Runs inside the Blancco rootfs (installed by blancco-init.sh as a systemd
|
||||
service). Follows the AlbusSeven journal and maps its log lines onto the
|
||||
same /imaging/status payload the WinPE imaging clients post, so each
|
||||
wiping PC gets a tile keyed by BIOS serial with pctype "blancco".
|
||||
|
||||
Stages (stage_total 7): 1 booting and 2 starting are posted by
|
||||
blancco-init.sh; this script posts 3 waiting for licence, 4 erasing,
|
||||
5 report saved, 6 report sent / on share, 7 done. A failed erasure step
|
||||
marks the session failed.
|
||||
|
||||
The final state comes from the report itself, not the journal: outside
|
||||
Blancco's debug mode Albus does not log "erasureFinishedForEntity",
|
||||
"Report saving success" or "Auto process reached the final step", so a
|
||||
watcher on /reports (where auto mode always saves the report) reads each
|
||||
new report and posts 7 "Wipe complete" or "Erasure FAILED".
|
||||
|
||||
Usage: blancco-progress.py [--replay FILE] [--dry-run]
|
||||
"""
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import urllib.request
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
STATUS_URL = "http://172.16.9.1:9009/imaging/status"
|
||||
CLOUD_URL = "http://172.16.9.1:9009/reports/cloud-status"
|
||||
STAGE_TOTAL = 7
|
||||
# Progress lines arrive in bursts; post at most this often unless the
|
||||
# stage itself changes.
|
||||
MIN_POST_INTERVAL = 3.0
|
||||
|
||||
|
||||
def read(path):
|
||||
try:
|
||||
with open(path) as f:
|
||||
return f.read().strip()
|
||||
except OSError:
|
||||
return ""
|
||||
|
||||
|
||||
def pxe_iface():
|
||||
"""(mac, ip) of the interface on the PXE LAN, or blanks."""
|
||||
try:
|
||||
out = subprocess.run(["ip", "-o", "-4", "addr"], capture_output=True,
|
||||
text=True, timeout=5).stdout
|
||||
except (OSError, subprocess.SubprocessError):
|
||||
return "", ""
|
||||
for line in out.splitlines():
|
||||
m = re.search(r"^\d+:\s+(\S+)\s+inet (172\.16\.9\.\d+)/", line)
|
||||
if m:
|
||||
return read(f"/sys/class/net/{m.group(1)}/address"), m.group(2)
|
||||
return "", ""
|
||||
|
||||
|
||||
REPORTS_DIR = "/reports"
|
||||
|
||||
|
||||
def report_outcome(path):
|
||||
"""(wiped: bool, failed_drives: list[str]) from a Blancco report XML.
|
||||
|
||||
Per drive: wiped if any of its erasures succeeded, so a failed NIST
|
||||
Purge followed by a successful Clear fallback still counts as wiped.
|
||||
None if the report has no erasures (hardware-only report).
|
||||
"""
|
||||
root = ET.parse(path).getroot()
|
||||
drives = {}
|
||||
for i, er in enumerate(root.iter("entries")):
|
||||
if er.get("name") != "erasure":
|
||||
continue
|
||||
state, serial, model = "", "", ""
|
||||
for e in er:
|
||||
if e.tag == "entry" and e.get("name") == "state":
|
||||
state = (e.text or "").strip()
|
||||
if e.tag == "entries" and e.get("name") == "target":
|
||||
for t in e:
|
||||
if t.get("name") == "serial":
|
||||
serial = (t.text or "").strip()
|
||||
elif t.get("name") == "model":
|
||||
model = (t.text or "").strip()
|
||||
key = serial or f"#{i}"
|
||||
ok = state.lower() == "successful"
|
||||
prev = drives.get(key, (False, ""))
|
||||
drives[key] = (prev[0] or ok, f"{model} {serial}".strip())
|
||||
if not drives:
|
||||
return None
|
||||
failed = [label for ok, label in drives.values() if not ok]
|
||||
return (not failed, failed)
|
||||
|
||||
|
||||
SERIAL = read("/sys/class/dmi/id/product_serial") or read("/etc/machine-id")
|
||||
MODEL = " ".join(x for x in (read("/sys/class/dmi/id/sys_vendor"),
|
||||
read("/sys/class/dmi/id/product_name")) if x)
|
||||
|
||||
# (pattern, stage_index, stage text or callable(match) -> text, status)
|
||||
RULES = [
|
||||
(re.compile(r"Waiting for MC connectivity"), 3,
|
||||
"Waiting for licence (cloud)", None),
|
||||
(re.compile(r"ClurichaunLicenseProvider: ([1-9]\d*) licenses available for product id 1\b"), 3,
|
||||
lambda m: f"Licence OK ({m.group(1)} left)", None),
|
||||
(re.compile(r"startErasure: success"), 4, "Erasing", None),
|
||||
(re.compile(r"About to start Step (\d+) \((.*?)\)\.?$"), 4,
|
||||
lambda m: f"Erasing: step {m.group(1)} - {m.group(2).split(' or ')[0][:60]}", None),
|
||||
(re.compile(r"Step \d+ \(.*\) failed|Erasure step .* failed\b|erasure failed", re.I), 4,
|
||||
"Erasure FAILED", "failed"),
|
||||
(re.compile(r"erasureFinishedForEntity"), 5, "Erasure finished", None),
|
||||
(re.compile(r"\[report\.manager\].*Report saving success"), 5, "Report saved", None),
|
||||
(re.compile(r"Report sent to MC"), 6, "Report sent to cloud", None),
|
||||
(re.compile(r"Auto process reached the final step"), 7, "Done", "succeeded"),
|
||||
]
|
||||
# Cloud upload of a report: "[comm.manager] Sending report document id: <id>"
|
||||
# then "Report sent to MC" (or an upload failure).
|
||||
SENDING = re.compile(r"Sending report document id: ([0-9a-fA-F-]{36})")
|
||||
SENT = re.compile(r"Report sent to MC")
|
||||
SEND_FAILED = re.compile(r"(report (upload|send|sending)[^\n]*fail|failed to (send|upload) report)", re.I)
|
||||
# Drive progress, e.g. "FORMAT NVM progress: 75%". Excludes the self-test
|
||||
# completion and volume lines that also carry a percentage.
|
||||
PROGRESS = re.compile(r"\bprogress[^%\d]{0,20}(\d{1,3})%", re.I)
|
||||
|
||||
|
||||
class Reporter:
|
||||
def __init__(self, dry_run=False):
|
||||
self.dry_run = dry_run
|
||||
self.mac, self.ip = pxe_iface()
|
||||
self.stage_index = 2
|
||||
self.stage = "Blancco started"
|
||||
self.status = "in_progress"
|
||||
self.last_post = 0.0
|
||||
self.pending = []
|
||||
|
||||
def post(self, force=False):
|
||||
now = time.monotonic()
|
||||
if not force and now - self.last_post < MIN_POST_INTERVAL:
|
||||
return
|
||||
payload = {
|
||||
"serial": SERIAL,
|
||||
"mac": self.mac,
|
||||
"ip": self.ip,
|
||||
"pctype": "blancco",
|
||||
"hostname_target": MODEL,
|
||||
"current_stage": self.stage,
|
||||
"stage_index": self.stage_index,
|
||||
"stage_total": STAGE_TOTAL,
|
||||
"status": self.status,
|
||||
"source": "blancco",
|
||||
}
|
||||
if self.pending:
|
||||
payload["log_lines"] = self.pending[-20:]
|
||||
if self.dry_run:
|
||||
print(json.dumps(payload))
|
||||
else:
|
||||
req = urllib.request.Request(
|
||||
STATUS_URL, data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=5).read()
|
||||
except OSError:
|
||||
return # webapp unreachable: keep the lines for the next post
|
||||
self.pending = []
|
||||
self.last_post = now
|
||||
|
||||
def cloud(self, document_id, sent, detail):
|
||||
"""Tell the webapp whether a report reached the Blancco cloud."""
|
||||
payload = {"document_id": document_id, "serial": SERIAL,
|
||||
"sent": sent, "detail": detail[:200]}
|
||||
if self.dry_run:
|
||||
print(json.dumps({"cloud": payload}))
|
||||
return
|
||||
req = urllib.request.Request(
|
||||
CLOUD_URL, data=json.dumps(payload).encode(),
|
||||
headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
urllib.request.urlopen(req, timeout=5).read()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def line(self, text):
|
||||
text = text.strip()
|
||||
if not text:
|
||||
return
|
||||
m = SENDING.search(text)
|
||||
if m:
|
||||
self.sending = m.group(1)
|
||||
elif getattr(self, "sending", None):
|
||||
if SENT.search(text):
|
||||
self.cloud(self.sending, True, text)
|
||||
self.sending = None
|
||||
elif SEND_FAILED.search(text):
|
||||
self.cloud(self.sending, False, text)
|
||||
self.sending = None
|
||||
for pat, idx, stage, status in RULES:
|
||||
m = pat.search(text)
|
||||
if not m:
|
||||
continue
|
||||
if self.status == "failed" and status != "failed":
|
||||
return # a failure sticks for the rest of the run
|
||||
if idx < self.stage_index and status != "failed":
|
||||
return # late licence chatter must not rewind the tile
|
||||
new_stage = stage(m) if callable(stage) else stage
|
||||
changed = (idx, new_stage) != (self.stage_index, self.stage)
|
||||
self.stage_index = max(self.stage_index, idx) if status != "failed" else idx
|
||||
self.stage = new_stage
|
||||
if status:
|
||||
self.status = status
|
||||
self.pending.append(text[:200])
|
||||
if changed:
|
||||
self.post(force=True)
|
||||
return
|
||||
m = PROGRESS.search(text)
|
||||
if m and self.stage_index == 4 and "Self-Test" not in text:
|
||||
base = self.stage.split(" [")[0]
|
||||
self.stage = f"{base} [{m.group(1)}%]"
|
||||
self.post()
|
||||
|
||||
|
||||
def finish(self, path):
|
||||
"""Final stage from a finished report on disk."""
|
||||
try:
|
||||
outcome = report_outcome(path)
|
||||
except (OSError, ET.ParseError):
|
||||
return False
|
||||
if outcome is None:
|
||||
return True # nothing erased in this report; keep watching
|
||||
wiped, failed = outcome
|
||||
name = os.path.basename(path)
|
||||
self.stage_index = 7
|
||||
if wiped:
|
||||
self.stage, self.status = "Wipe complete", "succeeded"
|
||||
else:
|
||||
self.stage, self.status = "Erasure FAILED: " + ", ".join(failed)[:120], "failed"
|
||||
self.pending.append(f"Report {name}: {'all drives wiped' if wiped else 'failed: ' + ', '.join(failed)}")
|
||||
self.post(force=True)
|
||||
return True
|
||||
|
||||
|
||||
def watch_reports(rep, interval=5, stable=5):
|
||||
"""Post the final stage for each new report Albus saves in /reports."""
|
||||
seen = set(glob.glob(os.path.join(REPORTS_DIR, "*.xml")))
|
||||
while True:
|
||||
time.sleep(interval)
|
||||
for path in sorted(glob.glob(os.path.join(REPORTS_DIR, "*.xml"))):
|
||||
if path in seen:
|
||||
continue
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except OSError:
|
||||
continue
|
||||
if st.st_size == 0 or time.time() - st.st_mtime < stable:
|
||||
continue # Albus may still be writing it
|
||||
if rep.finish(path):
|
||||
seen.add(path)
|
||||
|
||||
|
||||
def main():
|
||||
dry_run = "--dry-run" in sys.argv
|
||||
rep = Reporter(dry_run=dry_run)
|
||||
if "--outcome" in sys.argv:
|
||||
print(report_outcome(sys.argv[sys.argv.index("--outcome") + 1]))
|
||||
return
|
||||
if "--replay" in sys.argv:
|
||||
with open(sys.argv[sys.argv.index("--replay") + 1]) as f:
|
||||
for text in f:
|
||||
rep.line(text)
|
||||
rep.post(force=True)
|
||||
return
|
||||
rep.post(force=True)
|
||||
threading.Thread(target=watch_reports, args=(rep,), daemon=True).start()
|
||||
proc = subprocess.Popen(
|
||||
["journalctl", "-f", "-b", "-o", "cat", "-t", "AlbusSeven", "-n", "all"],
|
||||
stdout=subprocess.PIPE, text=True, bufsize=1)
|
||||
for text in proc.stdout:
|
||||
rep.line(text)
|
||||
if rep.status in ("succeeded", "failed") and rep.stage_index == STAGE_TOTAL:
|
||||
rep.post(force=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
28
playbook/certs/zscaler-root-ca.crt
Executable file
28
playbook/certs/zscaler-root-ca.crt
Executable file
@@ -0,0 +1,28 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIE0zCCA7ugAwIBAgIJANu+mC2Jt3uTMA0GCSqGSIb3DQEBCwUAMIGhMQswCQYD
|
||||
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8GA1UEBxMIU2FuIEpvc2Ux
|
||||
FTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMMWnNjYWxlciBJbmMuMRgw
|
||||
FgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG9w0BCQEWE3N1cHBvcnRA
|
||||
enNjYWxlci5jb20wHhcNMTQxMjE5MDAyNzU1WhcNNDIwNTA2MDAyNzU1WjCBoTEL
|
||||
MAkGA1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExETAPBgNVBAcTCFNhbiBK
|
||||
b3NlMRUwEwYDVQQKEwxac2NhbGVyIEluYy4xFTATBgNVBAsTDFpzY2FsZXIgSW5j
|
||||
LjEYMBYGA1UEAxMPWnNjYWxlciBSb290IENBMSIwIAYJKoZIhvcNAQkBFhNzdXBw
|
||||
b3J0QHpzY2FsZXIuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA
|
||||
qT7STSxZRTgEFFf6doHajSc1vk5jmzmM6BWuOo044EsaTc9eVEV/HjH/1DWzZtcr
|
||||
fTj+ni205apMTlKBW3UYR+lyLHQ9FoZiDXYXK8poKSV5+Tm0Vls/5Kb8mkhVVqv7
|
||||
LgYEmvEY7HPY+i1nEGZCa46ZXCOohJ0mBEtB9JVlpDIO+nN0hUMAYYdZ1KZWCMNf
|
||||
5J/aTZiShsorN2A38iSOhdd+mcRM4iNL3gsLu99XhKnRqKoHeH83lVdfu1XBeoQz
|
||||
z5V6gA3kbRvhDwoIlTBeMa5l4yRdJAfdpkbFzqiwSgNdhbxTHnYYorDzKfr2rEFM
|
||||
dsMU0DHdeAZf711+1CunuQIDAQABo4IBCjCCAQYwHQYDVR0OBBYEFLm33UrNww4M
|
||||
hp1d3+wcBGnFTpjfMIHWBgNVHSMEgc4wgcuAFLm33UrNww4Mhp1d3+wcBGnFTpjf
|
||||
oYGnpIGkMIGhMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTERMA8G
|
||||
A1UEBxMIU2FuIEpvc2UxFTATBgNVBAoTDFpzY2FsZXIgSW5jLjEVMBMGA1UECxMM
|
||||
WnNjYWxlciBJbmMuMRgwFgYDVQQDEw9ac2NhbGVyIFJvb3QgQ0ExIjAgBgkqhkiG
|
||||
9w0BCQEWE3N1cHBvcnRAenNjYWxlci5jb22CCQDbvpgtibd7kzAMBgNVHRMEBTAD
|
||||
AQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAw0NdJh8w3NsJu4KHuVZUrmZgIohnTm0j+
|
||||
RTmYQ9IKA/pvxAcA6K1i/LO+Bt+tCX+C0yxqB8qzuo+4vAzoY5JEBhyhBhf1uK+P
|
||||
/WVWFZN/+hTgpSbZgzUEnWQG2gOVd24msex+0Sr7hyr9vn6OueH+jj+vCMiAm5+u
|
||||
kd7lLvJsBu3AO3jGWVLyPkS3i6Gf+rwAp1OsRrv3WnbkYcFf9xjuaf4z0hRCrLN2
|
||||
xFNjavxrHmsH8jPHVvgc1VD0Opja0l/BRVauTrUaoW6tE+wFG5rEcPGS80jjHK4S
|
||||
pB5iDj2mUZH1T8lzYtuZy0ZPirxmtsk3135+CKNa2OCAhhFjE0xd
|
||||
-----END CERTIFICATE-----
|
||||
473
playbook/clonezilla-scripts/pxe-cz.sh
Normal file
473
playbook/clonezilla-scripts/pxe-cz.sh
Normal file
@@ -0,0 +1,473 @@
|
||||
#!/bin/bash
|
||||
# PXE Clonezilla: back up or restore a PC by its machine number.
|
||||
#
|
||||
# The iPXE menu (GetPxeScript.aspx, "Clonezilla: back up / restore this
|
||||
# PC") boots Clonezilla live with:
|
||||
# ocs_prerun="wget ... -O /tmp/pxe-cz.sh http://<server>/clonezilla-scripts/pxe-cz.sh"
|
||||
# ocs_repository="smb3://clonezilla:<password>@<server>/clonezilla/"
|
||||
# ocs_live_run="bash /tmp/pxe-cz.sh"
|
||||
# pxe_cz_mode=save|restore pxe_machine=<typed text> pxe_end=1
|
||||
# Clonezilla runs the prerun (this download), mounts the repository on
|
||||
# /home/partimag, then runs this script as root on the console.
|
||||
#
|
||||
# Images are named <machine>-<YYYY-MM-DD-HHMM> (server time, site zone),
|
||||
# with pxe-meta.txt (machine, serial, model, disk, date, Clonezilla
|
||||
# version, 32/64-bit, check result) next to Clonezilla's own files. The
|
||||
# PXE webapp (Tools > Clonezilla backups) reads both.
|
||||
#
|
||||
# Runs under Clonezilla live's bash (3.3.3 amd64 and 3.2.0-5 i686, both
|
||||
# Debian with coreutils, util-linux, dmidecode, wget). Plain ASCII only.
|
||||
|
||||
set -u
|
||||
|
||||
# PXE_CZ_* overrides exist only for testing this script off Clonezilla.
|
||||
OCSROOT="${PXE_CZ_OCSROOT:-/home/partimag}"
|
||||
CMDLINE_FILE="${PXE_CZ_CMDLINE:-/proc/cmdline}"
|
||||
SYS_BLOCK="${PXE_CZ_SYS_BLOCK:-/sys/block}"
|
||||
SITE_TZ="America/New_York"
|
||||
SERVER="$(sed -n 's/.*ocs_prerun="[^"]*http:\/\/\([^/":]*\).*/\1/p' "$CMDLINE_FILE" 2>/dev/null)"
|
||||
[ -n "$SERVER" ] || SERVER=172.16.9.1
|
||||
CMDLINE=" $(cat "$CMDLINE_FILE" 2>/dev/null) "
|
||||
|
||||
# --- output helpers -------------------------------------------------------
|
||||
|
||||
line() { echo "======================================================================"; }
|
||||
|
||||
banner() {
|
||||
echo
|
||||
line
|
||||
for l in "$@"; do echo " $l"; done
|
||||
line
|
||||
echo
|
||||
}
|
||||
|
||||
# Ask what to do at the end. Success: power off after 60 s unless a key is
|
||||
# pressed. Failure: wait, the operator has to read the message.
|
||||
finish() {
|
||||
local ok="$1" ans
|
||||
sync
|
||||
while true; do
|
||||
echo
|
||||
echo " [Enter] power off r = reboot s = shell (type exit to come back)"
|
||||
if [ "$ok" = "yes" ]; then
|
||||
echo -n " Powering off in 60 seconds... "
|
||||
if ! read -r -t 60 ans; then ans=""; echo; fi
|
||||
else
|
||||
echo -n " Choice: "
|
||||
read -r ans || ans=""
|
||||
fi
|
||||
case "$ans" in
|
||||
r|R) umount "$OCSROOT" 2>/dev/null; sync; reboot; sleep 30 ;;
|
||||
s|S) bash -i ;;
|
||||
*) umount "$OCSROOT" 2>/dev/null; sync; poweroff; sleep 30 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
fail() {
|
||||
banner "FAILED: $1" "${@:2}"
|
||||
finish no
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- machine number -------------------------------------------------------
|
||||
|
||||
# Letters, digits and dash, starting with a letter or digit, up to 32.
|
||||
valid_machine() {
|
||||
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9-]{0,31}$ ]]
|
||||
}
|
||||
|
||||
# The text typed at the iPXE prompt sits between pxe_machine= and the
|
||||
# pxe_end=1 sentinel, so a typed space shows up here and is rejected
|
||||
# instead of being split into another kernel parameter.
|
||||
machine_from_cmdline() {
|
||||
local rest
|
||||
case "$CMDLINE" in
|
||||
*" pxe_machine="*" pxe_end=1 "*) ;;
|
||||
*) echo ""; return ;;
|
||||
esac
|
||||
rest="${CMDLINE#* pxe_machine=}"
|
||||
echo "${rest%% pxe_end=1 *}"
|
||||
}
|
||||
|
||||
# Sets MACHINE (upper case). Not run in $(...): finish() reads the
|
||||
# keyboard and must write to the console.
|
||||
get_machine() {
|
||||
local m
|
||||
m="$(machine_from_cmdline)"
|
||||
if [ -z "$m" ] || ! valid_machine "$m"; then
|
||||
if [ -n "$m" ]; then
|
||||
banner "The machine number typed at the boot menu is not valid:" \
|
||||
" [$m]" "Use letters, digits and dash only (for example 6501 or WJ-6501)."
|
||||
else
|
||||
banner "No machine number was given at the boot menu."
|
||||
fi
|
||||
while true; do
|
||||
echo -n " Machine number (letters, digits, dash; empty = stop): "
|
||||
read -r m || m=""
|
||||
[ -n "$m" ] || finish no
|
||||
valid_machine "$m" && break
|
||||
echo " [$m] is not a valid machine number: use letters, digits and dash only."
|
||||
done
|
||||
fi
|
||||
MACHINE="$(echo "$m" | tr '[:lower:]' '[:upper:]')"
|
||||
}
|
||||
|
||||
# --- time -----------------------------------------------------------------
|
||||
|
||||
# The PC's clock is often wrong (dead CMOS battery on old PCs, local time
|
||||
# on Windows PCs), so take the time from the PXE server's HTTP Date header
|
||||
# and set the live system clock from it. Falls back to the PC clock.
|
||||
TIME_SOURCE="pc-clock"
|
||||
sync_clock() {
|
||||
local hdr epoch
|
||||
hdr="$(wget -q -S --spider -T 10 -t 2 "http://$SERVER/clonezilla-scripts/pxe-cz.sh" 2>&1 \
|
||||
| sed -n 's/^ *Date: *//p' | head -1)"
|
||||
[ -n "$hdr" ] || return 0
|
||||
epoch="$(date -u -d "$hdr" +%s 2>/dev/null)" || return 0
|
||||
[ -n "$epoch" ] || return 0
|
||||
date -u -s "@$epoch" >/dev/null 2>&1 && TIME_SOURCE="pxe-server"
|
||||
}
|
||||
|
||||
site_date() {
|
||||
if [ -e "/usr/share/zoneinfo/$SITE_TZ" ]; then
|
||||
TZ="$SITE_TZ" date "$@"
|
||||
else
|
||||
date "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- hardware -------------------------------------------------------------
|
||||
|
||||
dmi() {
|
||||
local v
|
||||
v="$(dmidecode -s "$1" 2>/dev/null | grep -v '^#' | head -1 | tr -d '\r' | sed 's/^ *//; s/ *$//')"
|
||||
echo "$v"
|
||||
}
|
||||
|
||||
# Whole disks a backup can come from or a restore can go to: no CD, loop,
|
||||
# RAM, zram, device-mapper, md, eMMC boot areas, or empty card readers.
|
||||
list_disks() {
|
||||
local d n size
|
||||
for d in "$SYS_BLOCK"/*; do
|
||||
n="${d##*/}"
|
||||
case "$n" in
|
||||
loop*|sr*|ram*|zram*|dm-*|md*|fd*|nbd*|*boot[0-9]|*rpmb) continue ;;
|
||||
esac
|
||||
size="$(cat "$d/size" 2>/dev/null)"
|
||||
[ -n "$size" ] && [ "$size" -gt 0 ] 2>/dev/null || continue
|
||||
echo "$n"
|
||||
done
|
||||
}
|
||||
|
||||
disk_bytes() { echo $(( $(cat "$SYS_BLOCK/$1/size" 2>/dev/null || echo 0) * 512 )); }
|
||||
|
||||
disk_model() {
|
||||
local m
|
||||
m="$(cat "$SYS_BLOCK/$1/device/model" 2>/dev/null | sed 's/^ *//; s/ *$//')"
|
||||
[ -n "$m" ] || m="$(lsblk -dn -o MODEL "/dev/$1" 2>/dev/null | sed 's/^ *//; s/ *$//')"
|
||||
[ -n "$m" ] || m="unknown model"
|
||||
echo "$m"
|
||||
}
|
||||
|
||||
disk_tran() { lsblk -dn -o TRAN "/dev/$1" 2>/dev/null | sed 's/ //g'; }
|
||||
|
||||
human() {
|
||||
local b="$1"
|
||||
awk -v b="$b" 'BEGIN { if (b >= 1e12) printf "%.1f TB", b/1e12; else printf "%.0f GB", b/1e9 }'
|
||||
}
|
||||
|
||||
describe_disk() {
|
||||
local n="$1" t
|
||||
t="$(disk_tran "$n")"
|
||||
echo "/dev/$n $(human "$(disk_bytes "$n")") $(disk_model "$n")${t:+ ($t)}"
|
||||
}
|
||||
|
||||
# pick_disk "prompt" -> disk name on stdout. One disk: that one.
|
||||
pick_disk() {
|
||||
local prompt="$1" disks n i ans
|
||||
mapfile -t disks < <(list_disks)
|
||||
if [ "${#disks[@]}" -eq 0 ]; then
|
||||
echo ""
|
||||
return
|
||||
fi
|
||||
if [ "${#disks[@]}" -eq 1 ]; then
|
||||
echo "${disks[0]}"
|
||||
return
|
||||
fi
|
||||
while true; do
|
||||
{
|
||||
echo
|
||||
echo " $prompt"
|
||||
i=1
|
||||
for n in "${disks[@]}"; do
|
||||
echo " $i) $(describe_disk "$n")"
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo -n " Number (1-${#disks[@]}): "
|
||||
} >&2
|
||||
read -r ans || ans=""
|
||||
if [[ "$ans" =~ ^[0-9]+$ ]] && [ "$ans" -ge 1 ] && [ "$ans" -le "${#disks[@]}" ]; then
|
||||
echo "${disks[$((ans - 1))]}"
|
||||
return
|
||||
fi
|
||||
echo " Type a number from the list." >&2
|
||||
done
|
||||
}
|
||||
|
||||
live_version() {
|
||||
local v=""
|
||||
if [ -e /etc/ocs/ocs-live.conf ]; then
|
||||
# shellcheck disable=SC1091
|
||||
v="$(. /etc/ocs/ocs-live.conf >/dev/null 2>&1; echo "${drbl_ocs_live_ver:-}")"
|
||||
fi
|
||||
[ -n "$v" ] || v="clonezilla $(dpkg-query -W -f='${Version}' clonezilla 2>/dev/null)"
|
||||
echo "$v"
|
||||
}
|
||||
|
||||
bits() {
|
||||
case "$(uname -m)" in
|
||||
x86_64|amd64) echo 64 ;;
|
||||
*) echo 32 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# set_meta FILE KEY VALUE: replace or add one key=value line.
|
||||
set_meta() {
|
||||
local f="$1" k="$2" v="$3" tmp
|
||||
v="$(echo "$v" | tr -d '\r\n')"
|
||||
tmp="$(mktemp /tmp/pxe-meta.XXXXXX)"
|
||||
[ -e "$f" ] && grep -v "^$k=" "$f" > "$tmp"
|
||||
echo "$k=$v" >> "$tmp"
|
||||
cat "$tmp" > "$f"
|
||||
rm -f "$tmp"
|
||||
}
|
||||
|
||||
get_meta() {
|
||||
sed -n "s/^$2=//p" "$1" 2>/dev/null | head -1
|
||||
}
|
||||
|
||||
repo_ready() {
|
||||
mountpoint -q "$OCSROOT" 2>/dev/null || grep -qs " $OCSROOT cifs " /proc/mounts
|
||||
}
|
||||
|
||||
# --- backup ---------------------------------------------------------------
|
||||
|
||||
do_save() {
|
||||
local m="$1" disk stamp img n meta rc chk
|
||||
disk="$(pick_disk "This PC has more than one disk. Which one do you want to back up?")"
|
||||
[ -n "$disk" ] || fail "No disk found to back up." "Check the disk is connected and shows in the BIOS."
|
||||
stamp="$(site_date +%Y-%m-%d-%H%M)"
|
||||
img="$m-$stamp"
|
||||
n=2
|
||||
while [ -e "$OCSROOT/$img" ]; do
|
||||
img="$m-$stamp-$n"
|
||||
n=$((n + 1))
|
||||
done
|
||||
|
||||
banner "BACKUP of machine $m" \
|
||||
"Disk: $(describe_disk "$disk")" \
|
||||
"Image: $img" \
|
||||
"To: \\\\$SERVER\\clonezilla\\$img"
|
||||
|
||||
ocs-sr -q2 -batch -j2 -z9p -i 0 -sfsck -scs -senc -p command savedisk "$img" "$disk"
|
||||
rc=$?
|
||||
|
||||
meta="$OCSROOT/$img/pxe-meta.txt"
|
||||
if [ ! -d "$OCSROOT/$img" ]; then
|
||||
fail "Clonezilla did not create the image $img (exit code $rc)." \
|
||||
"See the messages above. Nothing was saved."
|
||||
fi
|
||||
: > "$meta"
|
||||
set_meta "$meta" machine "$m"
|
||||
set_meta "$meta" date "$(site_date '+%Y-%m-%d %H:%M %Z')"
|
||||
set_meta "$meta" date_utc "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||
set_meta "$meta" time_source "$TIME_SOURCE"
|
||||
set_meta "$meta" serial "$(dmi system-serial-number)"
|
||||
set_meta "$meta" manufacturer "$(dmi system-manufacturer)"
|
||||
set_meta "$meta" model "$(dmi system-product-name)"
|
||||
set_meta "$meta" disk "$disk"
|
||||
set_meta "$meta" disk_model "$(disk_model "$disk")"
|
||||
set_meta "$meta" disk_size_bytes "$(disk_bytes "$disk")"
|
||||
set_meta "$meta" clonezilla "$(live_version)"
|
||||
set_meta "$meta" bits "$(bits)"
|
||||
if [ -d /sys/firmware/efi ]; then set_meta "$meta" firmware uefi; else set_meta "$meta" firmware bios; fi
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
set_meta "$meta" save failed
|
||||
set_meta "$meta" check not-run
|
||||
fail "Clonezilla could not save machine $m (exit code $rc)." \
|
||||
"The partial image $img is marked as failed on the share;" \
|
||||
"delete it in the webapp (Tools > Clonezilla backups) and try again."
|
||||
fi
|
||||
set_meta "$meta" save ok
|
||||
|
||||
banner "Checking the saved image $img ..."
|
||||
ocs-chkimg -b "$img"
|
||||
chk=$?
|
||||
set_meta "$meta" check_date "$(site_date '+%Y-%m-%d %H:%M %Z')"
|
||||
if [ "$chk" -ne 0 ]; then
|
||||
set_meta "$meta" check failed
|
||||
fail "The image $img was saved but its CHECK FAILED (exit code $chk)." \
|
||||
"Do not rely on it. Back this PC up again."
|
||||
fi
|
||||
set_meta "$meta" check ok
|
||||
sync
|
||||
banner "SUCCESS: machine $m backed up and checked." \
|
||||
"Image: $img" \
|
||||
"Serial: $(get_meta "$meta" serial) Model: $(get_meta "$meta" model)"
|
||||
finish yes
|
||||
}
|
||||
|
||||
# --- restore --------------------------------------------------------------
|
||||
|
||||
is_cz_image() {
|
||||
local d="$1"
|
||||
[ -d "$d" ] || return 1
|
||||
[ -f "$d/disk" ] || [ -f "$d/parts" ] || return 1
|
||||
local f
|
||||
for f in "$d"/*; do
|
||||
case "${f##*/}" in
|
||||
*-ptcl-img*|*.dd-img*|*-img.*|clonezilla-img) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# Images of one machine, newest first: <machine>-YYYY-MM-DD-HHMM[-n] by
|
||||
# name, plus any image whose pxe-meta.txt says machine=<machine>.
|
||||
machine_images() {
|
||||
local m="$1" d n mm
|
||||
for d in "$OCSROOT"/*/; do
|
||||
d="${d%/}"
|
||||
n="${d##*/}"
|
||||
is_cz_image "$d" || continue
|
||||
mm="$(get_meta "$d/pxe-meta.txt" machine | tr '[:lower:]' '[:upper:]')"
|
||||
if [ "$mm" = "$m" ]; then
|
||||
echo "$n"
|
||||
elif [ -z "$mm" ] && echo "$n" | tr '[:lower:]' '[:upper:]' | grep -qE "^$m-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}(-[0-9]+)?$"; then
|
||||
echo "$n"
|
||||
fi
|
||||
done | sort_newest
|
||||
}
|
||||
|
||||
# Sort names by their date stamp (the last YYYY-MM-DD-HHMM[-n]), newest first.
|
||||
sort_newest() {
|
||||
local n key
|
||||
while read -r n; do
|
||||
key="$(echo "$n" | grep -oE '[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{4}(-[0-9]+)?$')"
|
||||
echo "${key:-0000}|$n"
|
||||
done | sort -r | cut -d'|' -f2-
|
||||
}
|
||||
|
||||
image_disks() { tr -s ' \t' '\n' < "$OCSROOT/$1/disk" 2>/dev/null | grep -v '^$'; }
|
||||
|
||||
image_disk_bytes() {
|
||||
local img="$1" src="$2" b sec
|
||||
b="$(get_meta "$OCSROOT/$img/pxe-meta.txt" disk_size_bytes)"
|
||||
if [[ "$b" =~ ^[0-9]+$ ]]; then echo "$b"; return; fi
|
||||
# Clonezilla's sfdisk dump: "last-lba: N" (GPT) or the partitions' end.
|
||||
sec="$(sed -n 's/^last-lba: *//p' "$OCSROOT/$img/$src-pt.sf" 2>/dev/null | head -1)"
|
||||
if [[ "$sec" =~ ^[0-9]+$ ]]; then echo $(( (sec + 34) * 512 )); return; fi
|
||||
echo 0
|
||||
}
|
||||
|
||||
do_restore() {
|
||||
local m="$1" imgs i n ans img src srcb tgt tgtb meta chkopt rc mo se ck
|
||||
mapfile -t imgs < <(machine_images "$m")
|
||||
if [ "${#imgs[@]}" -eq 0 ]; then
|
||||
fail "No images for machine $m on \\\\$SERVER\\clonezilla." \
|
||||
"Check the machine number, or look in the webapp (Tools > Clonezilla backups)."
|
||||
fi
|
||||
while true; do
|
||||
echo
|
||||
echo " Images of machine $m (newest first):"
|
||||
i=1
|
||||
for n in "${imgs[@]}"; do
|
||||
meta="$OCSROOT/$n/pxe-meta.txt"
|
||||
mo="$(get_meta "$meta" model)"; se="$(get_meta "$meta" serial)"; ck="$(get_meta "$meta" check)"
|
||||
echo " $i) $n ${mo:-model ?} serial ${se:-?} check: ${ck:-?}"
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo -n " Image to restore (1-${#imgs[@]}, q = quit): "
|
||||
read -r ans || ans=q
|
||||
case "$ans" in q|Q) finish no ;; esac
|
||||
if [[ "$ans" =~ ^[0-9]+$ ]] && [ "$ans" -ge 1 ] && [ "$ans" -le "${#imgs[@]}" ]; then
|
||||
img="${imgs[$((ans - 1))]}"
|
||||
break
|
||||
fi
|
||||
echo " Type a number from the list."
|
||||
done
|
||||
meta="$OCSROOT/$img/pxe-meta.txt"
|
||||
if [ "$(get_meta "$meta" save)" = "failed" ]; then
|
||||
fail "The image $img is marked as a FAILED backup and cannot be restored."
|
||||
fi
|
||||
mapfile -t src < <(image_disks "$img")
|
||||
if [ "${#src[@]}" -ne 1 ]; then
|
||||
fail "The image $img holds ${#src[@]} disks (${src[*]:-none})." \
|
||||
"Restore it with Clonezilla: manual (boot menu)."
|
||||
fi
|
||||
srcb="$(image_disk_bytes "$img" "${src[0]}")"
|
||||
|
||||
tgt="$(pick_disk "Which disk do you want to restore $img to? (ALL DATA ON IT WILL BE ERASED)")"
|
||||
[ -n "$tgt" ] || fail "No disk found to restore to."
|
||||
tgtb="$(disk_bytes "$tgt")"
|
||||
if [ "$srcb" -gt 0 ] && [ "$tgtb" -lt "$srcb" ]; then
|
||||
fail "The target disk is SMALLER than the disk the image came from." \
|
||||
"Image disk: $(human "$srcb")" "Target disk: $(describe_disk "$tgt")" \
|
||||
"Use a disk at least as large, or Clonezilla: manual (expert mode, -icds)."
|
||||
fi
|
||||
|
||||
banner "RESTORE machine $m" \
|
||||
"Image: $img (saved $(get_meta "$meta" date))" \
|
||||
"From: $(get_meta "$meta" model) serial $(get_meta "$meta" serial) disk $(human "$srcb")" \
|
||||
"Target: $(describe_disk "$tgt")" \
|
||||
"" \
|
||||
"EVERYTHING on /dev/$tgt will be ERASED and replaced by this image." \
|
||||
"This PC: $(dmi system-product-name) serial $(dmi system-serial-number)"
|
||||
echo -n " Type YES (capitals) to erase /dev/$tgt and restore: "
|
||||
read -r ans || ans=""
|
||||
if [ "$ans" != "YES" ]; then
|
||||
banner "Cancelled. Nothing was written."
|
||||
finish no
|
||||
fi
|
||||
|
||||
# A larger target: -k1 recreates the partition table proportionally
|
||||
# (every partition scaled by target/source size) and -r grows each
|
||||
# filesystem to its new partition, so the extra space is used. The
|
||||
# default (-k0) copies the table as-is and leaves the rest unallocated.
|
||||
# The image was checked after it was saved; check again only if that
|
||||
# check did not pass.
|
||||
chkopt=""
|
||||
[ "$(get_meta "$meta" check)" = "ok" ] && chkopt="-scr"
|
||||
ocs-sr -g auto -e1 auto -e2 -r -j2 -k1 $chkopt -batch -p command restoredisk "$img" "$tgt"
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ]; then
|
||||
fail "Clonezilla could not restore $img to /dev/$tgt (exit code $rc)." \
|
||||
"The disk may be partly written: do not boot from it until a restore succeeds."
|
||||
fi
|
||||
sync
|
||||
banner "SUCCESS: $img restored to /dev/$tgt." "At the next start, boot this PC from its disk (not the network)."
|
||||
finish yes
|
||||
}
|
||||
|
||||
# --- main -----------------------------------------------------------------
|
||||
|
||||
MODE="$(echo "$CMDLINE" | sed -n 's/.* pxe_cz_mode=\([a-z]*\) .*/\1/p')"
|
||||
case "$MODE" in
|
||||
save|restore) ;;
|
||||
*) fail "Unknown mode [$MODE] on the kernel command line (pxe_cz_mode=save or restore)." ;;
|
||||
esac
|
||||
|
||||
if ! repo_ready; then
|
||||
fail "The backup share \\\\$SERVER\\clonezilla is not mounted on $OCSROOT." \
|
||||
"Check the PXE server is up and the clonezilla Samba account works" \
|
||||
"(playbook var clonezilla_smb_password), then boot this option again."
|
||||
fi
|
||||
|
||||
sync_clock
|
||||
MACHINE=""
|
||||
get_machine
|
||||
case "$MODE" in
|
||||
save) do_save "$MACHINE" ;;
|
||||
restore) do_restore "$MACHINE" ;;
|
||||
esac
|
||||
190
playbook/engineering/Install-EngineeringLayer.ps1
Normal file
190
playbook/engineering/Install-EngineeringLayer.ps1
Normal file
@@ -0,0 +1,190 @@
|
||||
# Install-EngineeringLayer.ps1 - install the Engineering Layer before the MCL
|
||||
# orchestrator's cleanup removes C:\Deploy, and make sure the employee gets
|
||||
# the "Configure my Engineering Layer" icon.
|
||||
#
|
||||
# THE RACE THIS SOLVES
|
||||
# The bulk-enrollment orchestrator registers a DeleteDeployDir startup task that
|
||||
# does "rmdir /s /q C:\Drivers & rmdir /s /q C:\GEA & rmdir /s /q C:\Deploy" on
|
||||
# the NEXT boot. The engineering payload (529 MB installer + .NET 3.5 sxs + VC++
|
||||
# redists, 653 MB total) lives under C:\Deploy\Applications\extra_engineering.
|
||||
# Standard/Engineer builds have no autologon, so a PC that reboots between OOBE
|
||||
# and the employee's first sign-in loses C:\Deploy before the first-logon
|
||||
# install - and with it this script, which used to be called from there too.
|
||||
# Then nothing installed, no error anywhere, and no icon.
|
||||
#
|
||||
# So, in two steps (both from FlatUnattendW10-engineer.xml):
|
||||
# specialize -StageOnly : copy the payload AND this script to
|
||||
# C:\PreInstall\engineering while C:\Deploy certainly exists.
|
||||
# first logon (no switch, run from C:\PreInstall\engineering): install
|
||||
# from the staged copy. C:\Deploy is not needed any more.
|
||||
# Run from C:\Deploy (an older unattend) it still stages first, as before.
|
||||
#
|
||||
# THE ICON
|
||||
# Engineering_Install_v3.1 copies "Configure my Engineering Layer.lnk" to
|
||||
# C:\Users\Public\Desktop. The employee clicks it once (on the GE network: it
|
||||
# maps V: and runs install_local_new.cmd from there), which writes
|
||||
# C:\TMP\DUU_Complete.log and deletes the Public icon. After the install this
|
||||
# script puts the icon back if it is missing and the PC is not configured
|
||||
# yet, and registers an Active Setup component: at each user's first sign-in
|
||||
# Windows runs Ensure-EngineeringIcon.ps1 in that user's session, which puts
|
||||
# the icon on that user's desktop if the PC is still not configured and the
|
||||
# Public icon is gone (someone else clicked it, or it never appeared).
|
||||
#
|
||||
# Idempotent. Safe to re-run: it skips the copy when the staged payload is
|
||||
# already complete and skips the install when the marker is present (the
|
||||
# icon check still runs).
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$Source = 'C:\Deploy\Applications\extra_engineering',
|
||||
# C:\PreInstall is already the convention for payload that must outlive
|
||||
# imaging - the unattend installs PowerShell 7 from C:\PreInstall\installers.
|
||||
[string]$StagingRoot = 'C:\PreInstall\engineering',
|
||||
[string]$LogDir = 'C:\Logs\Engineering',
|
||||
# Relative to StagingRoot. Empty: the newest
|
||||
# engineering_layer\Engineering_Install_v<version>.exe (uploaded on the
|
||||
# PXE webapp, engineer image > Engineering Layer).
|
||||
[string]$Installer = '',
|
||||
[switch]$StageOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
New-Item -ItemType Directory -Force -Path $LogDir | Out-Null
|
||||
$log = Join-Path $LogDir 'install-engineering-layer.log'
|
||||
$marker = Join-Path $StagingRoot '.installed'
|
||||
|
||||
$IconName = 'Configure my Engineering Layer.lnk'
|
||||
$IconSource = 'C:\TMP\Engineering_Install\Imaging\packages\' + $IconName
|
||||
$PublicIcon = 'C:\Users\Public\Desktop\' + $IconName
|
||||
$Configured = 'C:\TMP\DUU_Complete.log' # written by install_local_new.cmd
|
||||
$ActiveSetup = 'HKLM:\SOFTWARE\Microsoft\Active Setup\Installed Components\GEA-EngineeringLayerIcon'
|
||||
|
||||
function Write-EngLog {
|
||||
param([string]$Message, [string]$Level = 'INFO')
|
||||
$line = '[{0}] [{1}] [{2}] {3}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Level, $env:USERNAME, $Message
|
||||
Add-Content -Path $log -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
# Icon for whoever signs in next. Called after every install attempt that
|
||||
# left the layer installed (fresh or already marked).
|
||||
function Set-EngineeringIcon {
|
||||
if (Test-Path -LiteralPath $Configured) {
|
||||
Write-EngLog 'PC already configured (DUU_Complete.log) - no icon needed.'
|
||||
return
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath $IconSource)) {
|
||||
Write-EngLog "Icon source missing: $IconSource (installer layout changed?)" 'ERROR'
|
||||
return
|
||||
}
|
||||
if (Test-Path -LiteralPath $PublicIcon) {
|
||||
Write-EngLog 'Public desktop icon present.'
|
||||
} else {
|
||||
Copy-Item -LiteralPath $IconSource -Destination $PublicIcon -Force -ErrorAction SilentlyContinue
|
||||
if (Test-Path -LiteralPath $PublicIcon) { Write-EngLog 'Public desktop icon was missing - restored.' 'WARN' }
|
||||
else { Write-EngLog "Could not restore $PublicIcon" 'ERROR' }
|
||||
}
|
||||
|
||||
# Per-user fallback, run by Windows once per user at first sign-in.
|
||||
$ensure = Join-Path $StagingRoot 'Ensure-EngineeringIcon.ps1'
|
||||
$body = @(
|
||||
'# Written by Install-EngineeringLayer.ps1; run once per user by Active Setup.'
|
||||
'# Puts "Configure my Engineering Layer" on this user''s desktop when the PC'
|
||||
'# is not configured yet and the Public desktop icon is gone.'
|
||||
"`$src = '$IconSource'"
|
||||
"if (Test-Path -LiteralPath '$Configured') { exit 0 }"
|
||||
"if (Test-Path -LiteralPath '$PublicIcon') { exit 0 }"
|
||||
'if (-not (Test-Path -LiteralPath $src)) { exit 0 }'
|
||||
'$desk = [Environment]::GetFolderPath(''Desktop'')'
|
||||
'if ($desk) { Copy-Item -LiteralPath $src -Destination (Join-Path $desk (Split-Path $src -Leaf)) -Force -ErrorAction SilentlyContinue }'
|
||||
'exit 0'
|
||||
)
|
||||
Set-Content -Path $ensure -Value $body -Encoding ascii -Force
|
||||
New-Item -Path $ActiveSetup -Force | Out-Null
|
||||
Set-ItemProperty -Path $ActiveSetup -Name '(Default)' -Value 'GE Aerospace Engineering Layer icon'
|
||||
Set-ItemProperty -Path $ActiveSetup -Name 'Version' -Value '1,0'
|
||||
Set-ItemProperty -Path $ActiveSetup -Name 'IsInstalled' -Value 1 -Type DWord
|
||||
Set-ItemProperty -Path $ActiveSetup -Name 'StubPath' -Value ('powershell.exe -NoProfile -WindowStyle Hidden -ExecutionPolicy Bypass -File "' + $ensure + '"')
|
||||
Write-EngLog 'Active Setup registered: each new user gets the icon if the PC is still unconfigured.'
|
||||
}
|
||||
|
||||
Write-EngLog '=== Install-EngineeringLayer starting ==='
|
||||
Write-EngLog "Source: $Source"
|
||||
Write-EngLog "Staged: $StagingRoot"
|
||||
Write-EngLog "Running from: $PSCommandPath"
|
||||
|
||||
if (Test-Path -LiteralPath $marker) {
|
||||
Write-EngLog 'Marker present - Engineering Layer already installed.'
|
||||
if (-not $StageOnly) { Set-EngineeringIcon }
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- stage the payload out of C:\Deploy -------------------------------------
|
||||
function Find-Installer([string]$Dir) {
|
||||
Get-ChildItem -LiteralPath (Join-Path $Dir 'engineering_layer') -Filter 'Engineering_Install_v*.exe' -File -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Name -match '^Engineering_Install_v(\d+(\.\d+)*)\.exe$' } |
|
||||
Sort-Object { $v = ($_.Name -replace '^Engineering_Install_v', '') -replace '\.exe$', ''; if ($v -notmatch '\.') { $v += '.0' }; [version]$v } -Descending |
|
||||
Select-Object -First 1
|
||||
}
|
||||
if (-not $Installer) {
|
||||
$found = Find-Installer $StagingRoot
|
||||
if (-not $found) { $found = Find-Installer $Source }
|
||||
$Installer = if ($found) { 'engineering_layer\' + $found.Name } else { 'engineering_layer\Engineering_Install_v3.1.exe' }
|
||||
}
|
||||
Write-EngLog "Installer: $Installer"
|
||||
$stagedExe = Join-Path $StagingRoot $Installer
|
||||
|
||||
if (Test-Path -LiteralPath $stagedExe) {
|
||||
Write-EngLog 'Payload already staged outside C:\Deploy - skipping copy.'
|
||||
} else {
|
||||
if (-not (Test-Path -LiteralPath $Source)) {
|
||||
Write-EngLog "Source not found: $Source" 'ERROR'
|
||||
Write-EngLog 'C:\Deploy was removed (DeleteDeployDir) before the payload was staged. Re-stage from the share:' 'ERROR'
|
||||
Write-EngLog ' \\172.16.9.1\winpeapps\gea-engineer\Deploy\Applications\extra_engineering' 'ERROR'
|
||||
exit 2
|
||||
}
|
||||
Write-EngLog 'Copying payload out of C:\Deploy (this is the whole point - do not skip)'
|
||||
New-Item -ItemType Directory -Force -Path $StagingRoot | Out-Null
|
||||
# robocopy, not Copy-Item: 653 MB, restartable, and it reports properly.
|
||||
# Exit codes 0-7 are success, 8+ are failure - inverted vs everything else.
|
||||
# The copy includes this script, so first logon can run it from here.
|
||||
$rc = Start-Process -FilePath 'robocopy.exe' `
|
||||
-ArgumentList @("`"$Source`"", "`"$StagingRoot`"", '/E', '/R:2', '/W:5', '/NFL', '/NDL', '/NJH', '/NJS') `
|
||||
-Wait -PassThru -WindowStyle Hidden
|
||||
if ($rc.ExitCode -ge 8) {
|
||||
Write-EngLog "robocopy failed with exit $($rc.ExitCode) - payload NOT staged" 'ERROR'
|
||||
exit 3
|
||||
}
|
||||
Write-EngLog "Payload staged (robocopy exit $($rc.ExitCode))"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $stagedExe)) {
|
||||
Write-EngLog "Installer missing after staging: $stagedExe" 'ERROR'
|
||||
exit 4
|
||||
}
|
||||
|
||||
if ($StageOnly) {
|
||||
Write-EngLog 'StageOnly requested - payload is safe from cleanup, not installing.'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- install -----------------------------------------------------------------
|
||||
# The prereqs (.NET 3.5 sxs + VC++ redists) ship alongside; v3.1 installs them
|
||||
# itself and is idempotent, so they are staged for it rather than invoked here.
|
||||
Write-EngLog "Running $Installer (this takes a while - .NET 3.5, VC++, Cygwin, DUU)"
|
||||
try {
|
||||
$p = Start-Process -FilePath $stagedExe -ArgumentList '/S' -Wait -PassThru
|
||||
Write-EngLog "Installer exit code: $($p.ExitCode)"
|
||||
if ($p.ExitCode -eq 0 -or $p.ExitCode -eq 3010) {
|
||||
if ($p.ExitCode -eq 3010) { Write-EngLog 'Installer requests a reboot (3010)' 'WARN' }
|
||||
Set-Content -Path $marker -Value (Get-Date -Format 'o') -Encoding ascii -Force
|
||||
Write-EngLog 'Engineering Layer installed. Marker written.'
|
||||
Set-EngineeringIcon
|
||||
exit 0
|
||||
}
|
||||
Write-EngLog "Installer returned $($p.ExitCode) - NOT marking as installed" 'ERROR'
|
||||
exit 5
|
||||
} catch {
|
||||
Write-EngLog "Installer failed to start: $_" 'ERROR'
|
||||
exit 6
|
||||
}
|
||||
96
playbook/imaging-status/Send-ImagingStage.ps1
Normal file
96
playbook/imaging-status/Send-ImagingStage.ps1
Normal file
@@ -0,0 +1,96 @@
|
||||
# Send-ImagingStage.ps1 - posts one progress step of a gea-standard /
|
||||
# gea-engineer build to the PXE webapp's /imaging/status endpoint, so the
|
||||
# imaging floor follows these builds past WinPE (the shopfloor build has its
|
||||
# own reporting in Shopfloor\lib\Send-PxeStatus.ps1).
|
||||
#
|
||||
# Called from the image's FlatUnattendW10.xml (specialize RunSynchronous and
|
||||
# oobeSystem FirstLogonCommands). Keep those command lines short: Windows
|
||||
# rejects the whole unattend if a RunSynchronous Path exceeds 259 characters
|
||||
# (check with scripts/lint-unattend.py --local). Deployed to
|
||||
# _shared\Applications\extra\pxe-status\ on the PXE server, which PESetup
|
||||
# copies to C:\Deploy\Applications\extra\pxe-status\.
|
||||
#
|
||||
# Best-effort: never blocks or fails Windows setup. Every error is swallowed,
|
||||
# the script always exits 0, and each attempt is logged to
|
||||
# C:\Logs\send-imaging-stage.log.
|
||||
#
|
||||
# Three steps: 1 WinPE, 2 Windows setup installing apps, 3 ready at OOBE
|
||||
# (succeeded). Everything after OOBE needs the employee to log in, which
|
||||
# happens off the PXE network, so PXE cannot see it. Steps above
|
||||
# StageTotal (the old first-logon steps 4 and 5 still in the unattends) are
|
||||
# logged and not posted, so they cannot reopen a finished build.
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Stage,
|
||||
[int]$StageIndex = 0,
|
||||
[int]$StageTotal = 3,
|
||||
[string]$PCType = '',
|
||||
[ValidateSet('in_progress', 'succeeded', 'failed')][string]$Status = 'in_progress',
|
||||
[string]$PxeServer = '172.16.9.1',
|
||||
[int]$Port = 9009,
|
||||
[int]$TimeoutSec = 5,
|
||||
# Copy this script to C:\ProgramData\PxeStatus first: the bulk-enrollment
|
||||
# task deletes C:\Deploy before first logon, when the later steps run.
|
||||
[switch]$Persist
|
||||
)
|
||||
|
||||
$logFile = 'C:\Logs\send-imaging-stage.log'
|
||||
function Log([string]$msg) {
|
||||
try {
|
||||
if (-not (Test-Path 'C:\Logs')) { New-Item -ItemType Directory -Path 'C:\Logs' -Force | Out-Null }
|
||||
"$(Get-Date -Format s) $msg" | Out-File -FilePath $logFile -Append -Encoding utf8
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if ($Persist) {
|
||||
try {
|
||||
New-Item -ItemType Directory -Path 'C:\ProgramData\PxeStatus' -Force | Out-Null
|
||||
Copy-Item -LiteralPath $PSCommandPath -Destination 'C:\ProgramData\PxeStatus\Send-ImagingStage.ps1' -Force
|
||||
} catch { Log "persist failed: $($_.Exception.Message)" }
|
||||
}
|
||||
|
||||
if ($StageIndex -gt $StageTotal) {
|
||||
Log "SKIP stage $StageIndex of $StageTotal (after OOBE; PXE does not track it): $Stage"
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
$serial = $null
|
||||
try { $serial = (Get-CimInstance -ClassName Win32_BIOS -ErrorAction Stop).SerialNumber } catch {
|
||||
try { $serial = (Get-WmiObject -Class Win32_BIOS -ErrorAction Stop).SerialNumber } catch { }
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($serial)) { $serial = $env:COMPUTERNAME }
|
||||
$serial = ($serial -as [string]).Trim()
|
||||
|
||||
$mac = $null
|
||||
try {
|
||||
$nic = Get-NetAdapter -Physical -ErrorAction Stop |
|
||||
Where-Object { $_.Status -eq 'Up' -and $_.MediaType -eq '802.3' } | Select-Object -First 1
|
||||
if ($nic) { $mac = $nic.MacAddress -replace '-', ':' }
|
||||
} catch { }
|
||||
|
||||
$payload = @{
|
||||
serial = $serial
|
||||
hostname_target = $env:COMPUTERNAME
|
||||
current_stage = $Stage
|
||||
stage_index = $StageIndex
|
||||
stage_total = $StageTotal
|
||||
status = $Status
|
||||
}
|
||||
if ($PCType) { $payload.pctype = $PCType }
|
||||
if ($mac) { $payload.mac = $mac }
|
||||
$body = $payload | ConvertTo-Json -Compress
|
||||
$uri = "http://${PxeServer}:${Port}/imaging/status"
|
||||
|
||||
try {
|
||||
$resp = Invoke-WebRequest -Uri $uri -Method POST -Body $body -ContentType 'application/json' `
|
||||
-UseBasicParsing -TimeoutSec $TimeoutSec -ErrorAction Stop
|
||||
Log "OK $($resp.StatusCode) $body"
|
||||
} catch {
|
||||
# Off the imaging network (or the server is down): nothing to report to.
|
||||
Log "FAIL $($_.Exception.Message) $body"
|
||||
}
|
||||
} catch {
|
||||
Log "ERROR $($_.Exception.Message)"
|
||||
}
|
||||
exit 0
|
||||
198
playbook/ipxe/GetPxeScript.aspx
Normal file
198
playbook/ipxe/GetPxeScript.aspx
Normal file
@@ -0,0 +1,198 @@
|
||||
#!ipxe
|
||||
|
||||
set server 172.16.9.1
|
||||
|
||||
:menu
|
||||
menu GE Aerospace PXE Boot Menu
|
||||
item --gap -- ---- Windows Deployment ----
|
||||
item winpe Windows PE (Image Deployment)
|
||||
item --gap -- ---- Drive Erase ----
|
||||
item blancco Blancco Drive Eraser
|
||||
item blancco_debug Blancco Drive Eraser (debug / SSH)
|
||||
item --gap -- ---- Utilities ----
|
||||
item cz_backup Clonezilla: back up this PC
|
||||
item cz_restore Clonezilla: restore this PC
|
||||
item clonezilla Clonezilla: manual (USB or other location)
|
||||
item clonezilla32 Clonezilla: manual, 32-bit (XP-era PCs, forced)
|
||||
item --gap -- Memory test: Dell PCs press F12 at power-on, Diagnostics
|
||||
item --gap -- ----
|
||||
item reboot Reboot
|
||||
item exit Exit to BIOS
|
||||
choose --default winpe --timeout 30000 target && goto ${target}
|
||||
|
||||
:winpe
|
||||
# iPXE 2.0 reads the firmware variable; older iPXE cannot (empty), so it
|
||||
# warns as before. Secure Boot confirmed on: no warning, no wait.
|
||||
iseq ${efi/SecureBoot:uint8} 1 && goto winpe_boot ||
|
||||
iseq ${efi/SecureBoot:hex} 01 && goto winpe_boot ||
|
||||
echo
|
||||
echo Secure Boot is OFF on this PC (or this iPXE cannot tell).
|
||||
echo (iPXE read SecureBoot as: [${efi/SecureBoot:hex}] [${efi/SecureBoot:uint8}])
|
||||
echo Windows deployment requires Secure Boot to be ENABLED.
|
||||
echo If you disabled it earlier, re-enable it in BIOS Setup now.
|
||||
echo
|
||||
prompt --timeout 5000 Press any key to continue (auto-boot in 5s)... && goto winpe_boot || goto winpe_boot
|
||||
|
||||
:winpe_boot
|
||||
kernel http://${server}/win11/wimboot gui
|
||||
initrd http://${server}/win11/EFI/Microsoft/Boot/boot.stl EFI/Microsoft/Boot/Boot.stl
|
||||
initrd http://${server}/win11/EFI/Microsoft/Boot/BCD EFI/Microsoft/Boot/BCD
|
||||
initrd http://${server}/win11/EFI/Boot/bootx64.efi EFI/Boot/bootx64.efi
|
||||
initrd http://${server}/win11/Boot/boot.sdi Boot/boot.sdi
|
||||
initrd http://${server}/win11/sources/boot.wim sources/boot.wim
|
||||
# A command that fails without || aborts the whole iPXE script (the PC
|
||||
# drops to firmware with no explanation), so every boot line has one.
|
||||
boot || goto winpe_failed
|
||||
|
||||
:winpe_failed
|
||||
echo
|
||||
echo Windows PE boot FAILED (see the error above).
|
||||
echo Check that the PXE server is up and the win11 boot files are staged.
|
||||
prompt Press any key to return to menu... && goto menu || goto menu
|
||||
|
||||
# --- Clonezilla by machine number (back up / restore) ---------------------
|
||||
# The image share is \\${server}\clonezilla (Samba account clonezilla,
|
||||
# the playbook fills in its password). Clonezilla mounts it on
|
||||
# /home/partimag (ocs_repository, SMB 3), downloads
|
||||
# clonezilla-scripts/pxe-cz.sh (ocs_prerun) and runs it (ocs_live_run):
|
||||
# it checks the machine number, saves <machine>-<YYYY-MM-DD-HHMM> or lists
|
||||
# that machine's images to restore, and writes pxe-meta.txt.
|
||||
:cz_backup
|
||||
set cz_mode save
|
||||
goto cz_machine
|
||||
|
||||
:cz_restore
|
||||
set cz_mode restore
|
||||
goto cz_machine
|
||||
|
||||
:cz_machine
|
||||
# iPXE cannot check characters: pxe-cz.sh accepts letters, digits and dash
|
||||
# and asks again there. Empty: ask again. Esc / Ctrl-C: back to the menu.
|
||||
echo
|
||||
# iPXE drops a trailing space from a command line, so the space after the
|
||||
# colon comes from a setting (0x20) rather than the literal text.
|
||||
set sp:hex 20
|
||||
echo -n Machine number:${sp:string} && read czmachine || goto menu
|
||||
isset ${czmachine} || goto cz_machine
|
||||
# Same CPU check as the manual entry below (cpuid --ext 29 = 64-bit).
|
||||
cpuid --ext 29 && goto cz_auto64 || goto cz_auto32
|
||||
|
||||
:cz_auto64
|
||||
set base http://${server}/clonezilla
|
||||
kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales=en_US.UTF-8 keyboard-layouts=us ocs_prerun="wget -q -T 15 -t 5 -O /tmp/pxe-cz.sh http://${server}/clonezilla-scripts/pxe-cz.sh" ocs_repository="smb3://clonezilla:__CLONEZILLA_SMB_PASSWORD__@${server}/clonezilla/" echo_ocs_repository=no ocs_live_run="bash /tmp/pxe-cz.sh" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs pxe_cz_mode=${cz_mode} pxe_machine=${czmachine} pxe_end=1 || goto secureboot_warn
|
||||
initrd ${base}/initrd.img || goto boot_failed
|
||||
# Secure Boot ON: Debian's shim, as in :clonezilla64 below.
|
||||
shim ${base}/shimx64.efi ||
|
||||
boot || goto secureboot_warn
|
||||
|
||||
:cz_auto32
|
||||
# 3.2.0-5 i686 over legacy BIOS only; UEFI goes to the 64-bit build.
|
||||
iseq ${platform} efi && goto cz_auto64 ||
|
||||
set base http://${server}/clonezilla-i686
|
||||
kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales=en_US.UTF-8 keyboard-layouts=us ocs_prerun="wget -q -T 15 -t 5 -O /tmp/pxe-cz.sh http://${server}/clonezilla-scripts/pxe-cz.sh" ocs_repository="smb3://clonezilla:__CLONEZILLA_SMB_PASSWORD__@${server}/clonezilla/" echo_ocs_repository=no ocs_live_run="bash /tmp/pxe-cz.sh" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs pxe_cz_mode=${cz_mode} pxe_machine=${czmachine} pxe_end=1 || goto boot_failed
|
||||
initrd ${base}/initrd.img || goto boot_failed
|
||||
boot || goto boot_failed
|
||||
|
||||
# --- Clonezilla manual (interactive: USB, SSH, NFS or any share) ------------
|
||||
:clonezilla
|
||||
# 64-bit CPUs (cpuid extended bit 29 = long mode) get the current Clonezilla;
|
||||
# older 32-bit-only CPUs (Pentium 4 without EM64T, Pentium M, early Atom:
|
||||
# many XP-era PCs) get the last 32-bit release. If this iPXE has no cpuid
|
||||
# command the check fails and the 32-bit build is used, which also runs
|
||||
# on 64-bit CPUs.
|
||||
cpuid --ext 29 && goto clonezilla64 || goto clonezilla32
|
||||
|
||||
:clonezilla64
|
||||
set base http://${server}/clonezilla
|
||||
kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs || goto secureboot_warn
|
||||
initrd ${base}/initrd.img || goto boot_failed
|
||||
# Secure Boot ON: Clonezilla runs a Debian-signed kernel, started through
|
||||
# Debian's Microsoft-signed shim (from the Clonezilla release, staged as
|
||||
# clonezilla/shimx64.efi by prepare-boot-tools.sh), the same way Blancco
|
||||
# uses Ubuntu's. Its modules (usb-storage, uas, exfat) are signed by the
|
||||
# same build key, so USB drives work. Without the shim command (legacy
|
||||
# BIOS, old iPXE) the line fails harmlessly and the kernel boots directly.
|
||||
shim ${base}/shimx64.efi ||
|
||||
boot || goto secureboot_warn
|
||||
|
||||
:clonezilla32
|
||||
# Clonezilla 3.2.0-5 i686 (the last 32-bit release; later ones are amd64
|
||||
# only), for legacy-BIOS XP-era PCs. A UEFI PC is always 64-bit and its
|
||||
# firmware cannot start a 32-bit kernel, so UEFI goes to the 64-bit build.
|
||||
iseq ${platform} efi && goto clonezilla64 ||
|
||||
set base http://${server}/clonezilla-i686
|
||||
kernel ${base}/vmlinuz boot=live username=user union=overlay config components noswap edd=on nomodeset nodmraid locales= keyboard-layouts= ocs_live_run="ocs-live-general" ocs_live_extra_param="" ocs_live_batch=no net.ifnames=0 nosplash noprompt fetch=${base}/filesystem.squashfs || goto boot_failed
|
||||
initrd ${base}/initrd.img || goto boot_failed
|
||||
boot || goto boot_failed
|
||||
|
||||
:blancco
|
||||
iseq ${platform} pcbios && goto blancco_bios || goto blancco_efi
|
||||
|
||||
:blancco_bios
|
||||
echo Loading Blancco for legacy BIOS (canonical)...
|
||||
kernel http://${server}/blancco/vmlinuz-bde-linux initrd=initramfs-bde-linux.img,config.img,intel-ucode.img,amd-ucode.img quiet loglevel=3 archisobasedir=arch archiso_http_srv=http://${server}/blancco/ copytoram=y cow_spacesize=50% memtest=00 vmalloc=400M ip=dhcp libata.allow_tpm=1 modprobe.blacklist=btusb rd.udev.timeout=10 systemd.battery-check=0 || goto blancco_bios_failed
|
||||
initrd --name intel-ucode.img http://${server}/blancco/intel-ucode.img || goto blancco_bios_failed
|
||||
initrd --name amd-ucode.img http://${server}/blancco/amd-ucode.img || goto blancco_bios_failed
|
||||
initrd --name config.img http://${server}/blancco/config.img || goto blancco_bios_failed
|
||||
initrd --name initramfs-bde-linux.img http://${server}/blancco/initramfs-bde-linux.img || goto blancco_bios_failed
|
||||
boot || goto blancco_bios_failed
|
||||
:blancco_bios_failed
|
||||
echo
|
||||
echo Blancco BIOS boot FAILED.
|
||||
prompt Press any key to return to menu... && goto menu
|
||||
|
||||
:blancco_debug
|
||||
iseq ${platform} pcbios && goto blancco_bios ||
|
||||
echo Loading Blancco UEFI (debug: verbose, SSH for root)...
|
||||
kernel http://${server}/blancco/vmlinuz-ubuntu-70 initrd=kexec-initrd-70.img ip=dhcp nomodeset vga=normal console=tty0 i915.modeset=0 nouveau.modeset=0 modprobe.blacklist=nouveau,nvidiafb,nvidia,nvidia_drm,mei_me,btusb,dcdbas libata.allow_tpm=1 rd.udev.timeout=10 efi=noruntime usbcore.autosuspend=-1 pcie_aspm=off e1000e.IntMode=0 e1000e.eee_enable=0 consoleblank=0 systemd.battery-check=0 systemd.mask=suspend.target,sleep.target,hibernate.target,hybrid-sleep.target no_console_suspend mem_sleep_default=s2idle blancco.debug=1 || goto blancco_efi_grub
|
||||
initrd --name intel-ucode.img http://${server}/blancco/intel-ucode.img || goto blancco_efi_grub
|
||||
initrd --name amd-ucode.img http://${server}/blancco/amd-ucode.img || goto blancco_efi_grub
|
||||
initrd --name kexec-initrd-70.img http://${server}/blancco/kexec-initrd-70.img || goto blancco_efi_grub
|
||||
# Secure Boot ON: iPXE 2.0 starts the Canonical-signed kernel through
|
||||
# Ubuntu's Microsoft-signed shim. Older iPXE has no shim command: the
|
||||
# line fails harmlessly and the kernel boots directly (Secure Boot off).
|
||||
shim http://${server}/blancco/shimx64.efi ||
|
||||
boot || goto blancco_efi_grub
|
||||
|
||||
:blancco_efi
|
||||
echo Loading Blancco UEFI (direct kernel)...
|
||||
kernel http://${server}/blancco/vmlinuz-ubuntu-70 initrd=kexec-initrd-70.img ip=dhcp quiet loglevel=3 nomodeset vga=normal console=tty0 i915.modeset=0 nouveau.modeset=0 modprobe.blacklist=nouveau,nvidiafb,nvidia,nvidia_drm,mei_me,btusb,dcdbas libata.allow_tpm=1 rd.udev.timeout=10 efi=noruntime usbcore.autosuspend=-1 pcie_aspm=off e1000e.IntMode=0 e1000e.eee_enable=0 consoleblank=0 systemd.battery-check=0 systemd.mask=suspend.target,sleep.target,hibernate.target,hybrid-sleep.target no_console_suspend mem_sleep_default=s2idle || goto blancco_efi_grub
|
||||
initrd --name intel-ucode.img http://${server}/blancco/intel-ucode.img || goto blancco_efi_grub
|
||||
initrd --name amd-ucode.img http://${server}/blancco/amd-ucode.img || goto blancco_efi_grub
|
||||
initrd --name kexec-initrd-70.img http://${server}/blancco/kexec-initrd-70.img || goto blancco_efi_grub
|
||||
# Secure Boot ON: iPXE 2.0 starts the Canonical-signed kernel through
|
||||
# Ubuntu's Microsoft-signed shim. Older iPXE has no shim command: the
|
||||
# line fails harmlessly and the kernel boots directly (Secure Boot off).
|
||||
shim http://${server}/blancco/shimx64.efi ||
|
||||
boot || goto blancco_efi_grub
|
||||
:blancco_efi_grub
|
||||
echo Direct kernel boot failed - trying GRUB chain fallback...
|
||||
chain http://${server}/blancco/grubx64.efi || goto secureboot_warn
|
||||
|
||||
:boot_failed
|
||||
echo
|
||||
echo Download FAILED (see the error above). Check the PXE server and retry.
|
||||
prompt Press any key to return to menu... && goto menu || goto menu
|
||||
|
||||
:secureboot_warn
|
||||
echo
|
||||
echo ======================================================
|
||||
echo This option could not start with Secure Boot enabled.
|
||||
echo
|
||||
echo 1. Reboot this machine
|
||||
echo 2. Press F2 / Del to enter BIOS Setup
|
||||
echo 3. Disable Secure Boot
|
||||
echo 4. Save and exit BIOS
|
||||
echo 5. PXE boot again and select this option
|
||||
echo
|
||||
echo Re-enable Secure Boot after completing the task.
|
||||
echo ======================================================
|
||||
echo
|
||||
prompt Press any key to return to menu...
|
||||
goto menu
|
||||
|
||||
:reboot
|
||||
reboot
|
||||
|
||||
:exit
|
||||
exit
|
||||
@@ -1,23 +0,0 @@
|
||||
@echo off
|
||||
REM Install-AcroReader.cmd - Install Adobe Acrobat Reader DC + DC update patch
|
||||
REM
|
||||
REM Two-step install: base MSI with enterprise transform, then DC update patch.
|
||||
REM Same version as the GEA-Engineering image (specialize pass Orders 22-23).
|
||||
REM Files staged by startnet.cmd from enrollment share to C:\PreInstall\installers\adobe\.
|
||||
|
||||
echo Installing Adobe Acrobat Reader DC...
|
||||
msiexec /i "%~dp0AcroRead.msi" TRANSFORMS="%~dp0AcroRead.mst" /quiet /norestart
|
||||
if %errorlevel% neq 0 if %errorlevel% neq 3010 (
|
||||
echo Acrobat Reader MSI failed with exit code %errorlevel%
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo Applying Adobe Reader DC update patch...
|
||||
msiexec /p "%~dp0AcroRdrDCUpd2500120531.msp" /quiet /norestart
|
||||
if %errorlevel% neq 0 if %errorlevel% neq 3010 (
|
||||
echo Acrobat Reader patch failed with exit code %errorlevel%
|
||||
exit /b %errorlevel%
|
||||
)
|
||||
|
||||
echo Adobe Acrobat Reader DC installed successfully.
|
||||
exit /b 0
|
||||
@@ -1,43 +0,0 @@
|
||||
@echo off
|
||||
REM Install-KioskApp.cmd - Install Lobby Display or Dashboard kiosk app
|
||||
REM
|
||||
REM Reads C:\Enrollment\display-type.txt to determine which app to install.
|
||||
REM Only runs on Display PCs (PCTypes filter in preinstall.json).
|
||||
REM Both installers live alongside this script in the staged display\ subtree.
|
||||
|
||||
set "TYPEFILE=C:\Enrollment\display-type.txt"
|
||||
|
||||
if not exist "%TYPEFILE%" (
|
||||
echo No display-type.txt found - skipping kiosk app install.
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
set /p DISPLAYTYPE=<"%TYPEFILE%"
|
||||
|
||||
if /i "%DISPLAYTYPE%"=="Lobby" (
|
||||
set "INSTALLER=%~dp0GEAerospaceLobbyDisplaySetup.exe"
|
||||
set "APPNAME=Lobby Display"
|
||||
) else if /i "%DISPLAYTYPE%"=="Dashboard" (
|
||||
set "INSTALLER=%~dp0GEAerospaceDashboardSetup.exe"
|
||||
set "APPNAME=Dashboard"
|
||||
) else (
|
||||
echo Unknown display type: %DISPLAYTYPE%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
if not exist "%INSTALLER%" (
|
||||
echo %APPNAME% installer not found: %INSTALLER%
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Installing %APPNAME%...
|
||||
"%INSTALLER%" /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /LOG="C:\Logs\PreInstall\%APPNAME%-install.log"
|
||||
set EXITCODE=%errorlevel%
|
||||
|
||||
if %EXITCODE% equ 0 (
|
||||
echo %APPNAME% installed successfully.
|
||||
) else (
|
||||
echo %APPNAME% exited with code %EXITCODE%.
|
||||
)
|
||||
|
||||
exit /b %EXITCODE%
|
||||
@@ -1,46 +0,0 @@
|
||||
@echo off
|
||||
REM Install-Drivers.cmd - Adds bundled INF driver packages to the Windows driver store
|
||||
REM via pnputil. Runs from C:\PreInstall\installers\drivers\ where each vendor
|
||||
REM subdir (prolific\, startech\) sits alongside this script. /subdirs walks into
|
||||
REM them so one call covers every bundled driver.
|
||||
REM
|
||||
REM Writes C:\ProgramData\PXEDrivers\drivers-installed.marker on success so the
|
||||
REM preinstall runner's File DetectionMethod can skip re-runs.
|
||||
|
||||
setlocal
|
||||
|
||||
set LOGDIR=C:\Logs\PreInstall
|
||||
set LOG=%LOGDIR%\Install-Drivers.log
|
||||
set MARKER_DIR=C:\ProgramData\PXEDrivers
|
||||
set MARKER=%MARKER_DIR%\drivers-installed.marker
|
||||
|
||||
if not exist "%LOGDIR%" mkdir "%LOGDIR%"
|
||||
if not exist "%MARKER_DIR%" mkdir "%MARKER_DIR%"
|
||||
|
||||
echo ================================================================ >> "%LOG%"
|
||||
echo [%date% %time%] Install-Drivers session start >> "%LOG%"
|
||||
echo Source dir: %~dp0 >> "%LOG%"
|
||||
echo ================================================================ >> "%LOG%"
|
||||
|
||||
REM pnputil /add-driver with /subdirs recurses into vendor subdirs. /install
|
||||
REM binds the driver to any matching hardware present now; drivers without a
|
||||
REM matching device still land in the driver store and bind on plug-in later.
|
||||
pnputil.exe /add-driver "%~dp0*.inf" /subdirs /install >> "%LOG%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
echo [%date% %time%] pnputil exit code: %RC% >> "%LOG%"
|
||||
|
||||
REM pnputil exit codes:
|
||||
REM 0 success
|
||||
REM 259 ERROR_NO_MORE_ITEMS - no INFs matched (treat as failure)
|
||||
REM 3010 at least one driver installed, reboot recommended (treat as success)
|
||||
if "%RC%"=="0" goto :ok
|
||||
if "%RC%"=="3010" goto :ok
|
||||
|
||||
echo [%date% %time%] FAILED (exit %RC%) >> "%LOG%"
|
||||
exit /b %RC%
|
||||
|
||||
:ok
|
||||
echo [%date% %time%] SUCCESS >> "%LOG%"
|
||||
echo installed %date% %time% > "%MARKER%"
|
||||
exit /b 0
|
||||
@@ -1,38 +0,0 @@
|
||||
# OpenText HostExplorer ShopFloor - install scripts
|
||||
|
||||
These three files are git-tracked snapshots of what lives at runtime
|
||||
in `/home/camp/pxe-images/main/dependencies/opentext/`. The full
|
||||
runtime tree also contains the bundled installer binaries (~106 MB
|
||||
total) which are intentionally NOT in git:
|
||||
|
||||
```
|
||||
OpenTextHostExplorer15x64.msi ~16 MB
|
||||
OpenTextHostExplorer15x64.cab ~86 MB
|
||||
OpenTextHostExplorer15x64_ServicePack1.msp ~4.4 MB
|
||||
ShopFloorx64.mst ~20 KB
|
||||
Profile/ 5 .hep connection profiles
|
||||
Accessories/EB/ 6 .ebs/.ebx macros
|
||||
HostExplorer/Keymap/ 2 .kmv keymaps
|
||||
HostExplorer/Menu/ 2 .hmv menu layouts
|
||||
W10shortcuts/ 4 .lnk public-desktop shortcuts
|
||||
```
|
||||
|
||||
The canonical source for everything (scripts AND binaries) is
|
||||
`/home/camp/pxe-images/main/dependencies/opentext/`. The files in
|
||||
this dir are mirrors that exist so changes to the install logic and
|
||||
the version stamp end up in git history. When editing:
|
||||
|
||||
1. Edit `/home/camp/pxe-images/main/dependencies/opentext/Setup-OpenText.ps1`
|
||||
(or `.cmd` / `version.txt`)
|
||||
2. Re-run `bash playbook/sync-preinstall.sh` to push the runtime tree
|
||||
to the live PXE server
|
||||
3. `cp` the changed file(s) into `playbook/preinstall/opentext/` here
|
||||
so git picks up the change
|
||||
4. Re-upload the changed file(s) to Azure Blob at
|
||||
`prod/main/dependencies/opentext/<filename>`
|
||||
|
||||
To bump OpenText version: edit `version.txt` only - the value flows
|
||||
through to Setup-OpenText.ps1 (which reads it at runtime), to
|
||||
Install-OpenText.ps1 (the DSC wrapper, which downloads version.txt
|
||||
first as a cheap detection check), and to the registry marker at
|
||||
`HKLM:\SOFTWARE\GE\OpenText\Installed`.
|
||||
@@ -1,11 +0,0 @@
|
||||
@echo off
|
||||
REM Setup-OpenText.cmd - tiny launcher for Setup-OpenText.ps1
|
||||
REM
|
||||
REM The PXE PreInstall runner only invokes Type:MSI or Type:EXE entries from
|
||||
REM preinstall.json. This .cmd lives next to Setup-OpenText.ps1 in the staged
|
||||
REM opentext\ subtree and just hands off to PowerShell with bypass policy.
|
||||
REM Both files end up at C:\PreInstall\installers\opentext\ via xcopy /E from
|
||||
REM the WinPE staging step in startnet.cmd.
|
||||
|
||||
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Setup-OpenText.ps1"
|
||||
exit /b %errorlevel%
|
||||
@@ -1,361 +0,0 @@
|
||||
#!/usr/bin/env pwsh
|
||||
# Setup-OpenText.ps1 - OpenText HostExplorer 15 SP1 ShopFloor installer + profile
|
||||
# deployment, callable from both PXE PreInstall and Intune DSC paths.
|
||||
#
|
||||
# WHY THIS EXISTS:
|
||||
# The vendor-supplied OpenText.exe (Inno Setup wrapper built by WJDT) bundles
|
||||
# the install steps but its [Files] section deploys per-user content to
|
||||
# {userappdata} - which resolves to SYSTEM's profile under DSC and to a single
|
||||
# user under PreInstall. As a result the operator (logging in via Azure AD)
|
||||
# never sees the profiles, keymaps, menus, or macros - only the installed
|
||||
# binaries. This script replaces OpenText.exe entirely, doing the same install
|
||||
# steps via direct msiexec calls AND fanning the per-user content out to:
|
||||
# - %ProgramData%\Hummingbird\Connectivity\15.00\Shared\
|
||||
# - C:\Users\Default\AppData\Roaming\Hummingbird\Connectivity\15.00\
|
||||
# - Each existing user profile under C:\Users\
|
||||
#
|
||||
# INVOKED BY:
|
||||
# - PreInstall: Setup-OpenText.cmd wrapper (because the runner only knows MSI/EXE)
|
||||
# - DSC: Install-OpenText.ps1 downloads the bundled tree from blob, then
|
||||
# invokes this script with -SourceDir <temp dir>
|
||||
#
|
||||
# DETECTION:
|
||||
# Skips if HKLM:\SOFTWARE\GE\OpenText\Installed = $expectedVersion. Marker is
|
||||
# written at the end of a successful run, so the runner / DSC wrapper can
|
||||
# no-op on subsequent invocations.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
# Override when invoked from a temp dir (DSC path) where the bundled files were
|
||||
# just downloaded. Defaults to $PSScriptRoot, but resolved INSIDE the script body
|
||||
# below - PowerShell evaluates `param([string]$X = $PSScriptRoot)` at parameter-
|
||||
# binding time, when $PSScriptRoot may not yet be populated, so the default winds
|
||||
# up as an empty string. Setting it in the body works because $PSScriptRoot is
|
||||
# reliably populated by then.
|
||||
[string]$SourceDir
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if (-not $SourceDir) {
|
||||
$SourceDir = $PSScriptRoot
|
||||
}
|
||||
|
||||
# Normalize $SourceDir to a canonical absolute path. The CMD shim passes
|
||||
# "%~dp0..\apps\opentext" which embeds a literal "..". msiexec / the Windows
|
||||
# Installer service fail to open the package with that unresolved segment
|
||||
# (exit 1619, ERROR_INSTALL_PACKAGE_OPEN_FAILED), even though every other
|
||||
# .NET / PowerShell API resolves it fine. Resolve-Path collapses ".." into
|
||||
# a clean drive-rooted path before any msiexec invocation.
|
||||
if (Test-Path -LiteralPath $SourceDir) {
|
||||
try { $SourceDir = (Resolve-Path -LiteralPath $SourceDir).ProviderPath } catch {}
|
||||
}
|
||||
|
||||
# --- Inline site-config reader (this script runs from C:\PreInstall\installers\opentext\,
|
||||
# NOT from C:\Enrollment\shopfloor-setup\, so it can't dot-source Get-PCProfile.ps1) ---
|
||||
function Get-SiteConfig {
|
||||
$configPath = 'C:\Enrollment\site-config.json'
|
||||
if (-not (Test-Path $configPath)) { return $null }
|
||||
try {
|
||||
return Get-Content $configPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# --- Logging (set up FIRST so any startup error - missing version.txt, broken
|
||||
# bundled file, etc. - lands in the log file instead of disappearing into the
|
||||
# runner's stdout void) ---
|
||||
$logDir = 'C:\Logs\PreInstall'
|
||||
$logFile = Join-Path $logDir 'Setup-OpenText.log'
|
||||
$msiLog = Join-Path $logDir 'Setup-OpenText-msi.log'
|
||||
$mspLog = Join-Path $logDir 'Setup-OpenText-msp.log'
|
||||
|
||||
if (-not (Test-Path $logDir)) {
|
||||
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-SetupLog {
|
||||
param([string]$Message)
|
||||
$line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-SetupLog "================================================================"
|
||||
Write-SetupLog "=== Setup-OpenText.ps1 starting ==="
|
||||
Write-SetupLog "================================================================"
|
||||
Write-SetupLog "SourceDir: $SourceDir"
|
||||
Write-SetupLog "PSScriptRoot: $PSScriptRoot"
|
||||
Write-SetupLog "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
# --- Config ---
|
||||
# Version is read from version.txt next to this script. ONE source of truth: bumping
|
||||
# version.txt is the only edit needed when shipping a new OpenText build. Setup-
|
||||
# OpenText.ps1 itself, Install-OpenText.ps1 (DSC wrapper), and the registry marker
|
||||
# all derive their notion of "expected version" from this file.
|
||||
$versionFile = Join-Path $SourceDir 'version.txt'
|
||||
Write-SetupLog "Looking for version.txt at: $versionFile"
|
||||
if (-not (Test-Path $versionFile)) {
|
||||
Write-SetupLog "ERROR: version.txt not found in $SourceDir - cannot determine expected version."
|
||||
Write-SetupLog "Directory listing of $SourceDir :"
|
||||
if (Test-Path $SourceDir) {
|
||||
Get-ChildItem $SourceDir -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-SetupLog " $($_.Name) $($_.Length) bytes"
|
||||
}
|
||||
} else {
|
||||
Write-SetupLog " (SourceDir does not exist)"
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
$expectedVersion = (Get-Content -Path $versionFile -Raw -ErrorAction Stop).Trim()
|
||||
if (-not $expectedVersion) {
|
||||
Write-SetupLog "ERROR: version.txt at $versionFile is empty."
|
||||
exit 1
|
||||
}
|
||||
Write-SetupLog "Expected version (from version.txt): $expectedVersion"
|
||||
|
||||
# --- Detection: skip if already deployed at expected version ---
|
||||
$markerKey = 'HKLM:\SOFTWARE\GE\OpenText'
|
||||
$markerVal = 'Installed'
|
||||
if (Test-Path $markerKey) {
|
||||
$installed = (Get-ItemProperty -Path $markerKey -Name $markerVal -ErrorAction SilentlyContinue).$markerVal
|
||||
if ($installed -eq $expectedVersion) {
|
||||
Write-SetupLog "OpenText $expectedVersion already deployed (marker present) - skipping."
|
||||
exit 0
|
||||
}
|
||||
Write-SetupLog "OpenText marker mismatch (found '$installed', expected '$expectedVersion') - re-deploying."
|
||||
}
|
||||
else {
|
||||
Write-SetupLog "OpenText marker not present - first install."
|
||||
}
|
||||
|
||||
# --- Verify bundled files are where we expect ---
|
||||
$msiPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64.msi'
|
||||
$cabPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64.cab'
|
||||
$mspPath = Join-Path $SourceDir 'OpenTextHostExplorer15x64_ServicePack1.msp'
|
||||
$mstPath = Join-Path $SourceDir 'ShopFloorx64.mst'
|
||||
|
||||
foreach ($f in @($msiPath, $cabPath, $mspPath, $mstPath)) {
|
||||
if (-not (Test-Path $f)) {
|
||||
Write-SetupLog "ERROR: required file not found: $f"
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
# --- Step 1: Install the base MSI with the ShopFloor transform ---
|
||||
# NOTE: We deliberately do NOT pass REBOOT=ReallySuppress here even though we do
|
||||
# for the VC++ MSIs. OpenText HostExplorer installs shell extensions that hook
|
||||
# explorer.exe, and the MSI uses Restart Manager to ask explorer to close so the
|
||||
# in-use shell DLLs can be replaced. With REBOOT=ReallySuppress, RM closes
|
||||
# explorer.exe but interprets "restart explorer" as a reboot action and refuses
|
||||
# to relaunch it - leaving the user without a desktop. /norestart on its own
|
||||
# prevents the actual Windows reboot but lets RM cleanly close-and-relaunch
|
||||
# explorer mid-install. msiexec still returns 3010 ("reboot would be needed"),
|
||||
# which we treat as success below.
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 1: Installing OpenTextHostExplorer15x64.msi with ShopFloorx64.mst..."
|
||||
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$msiArgs = "/i `"$msiPath`" TRANSFORMS=`"$mstPath`" /qn /norestart /L*v `"$msiLog`""
|
||||
Write-SetupLog " msiexec.exe $msiArgs"
|
||||
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.FileName = 'msiexec.exe'
|
||||
$psi.Arguments = $msiArgs
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
$proc.WaitForExit()
|
||||
$msiExit = $proc.ExitCode
|
||||
Write-SetupLog " msiexec exit code: $msiExit"
|
||||
|
||||
if ($msiExit -ne 0 -and $msiExit -ne 3010) {
|
||||
Write-SetupLog "ERROR: base MSI install failed (exit $msiExit). See $msiLog"
|
||||
exit 1
|
||||
}
|
||||
if ($msiExit -eq 3010) {
|
||||
Write-SetupLog " (3010 = reboot needed but suppressed)"
|
||||
}
|
||||
|
||||
# --- Step 2: Apply Service Pack 1 patch ---
|
||||
# Same Restart Manager rationale as Step 1 - skip REBOOT=ReallySuppress so RM
|
||||
# can relaunch explorer.exe after replacing the patched shell extension DLLs.
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 2: Applying SP1 patch..."
|
||||
if (Test-Path $mspLog) { Remove-Item $mspLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
$mspArgs = "/p `"$mspPath`" /qn /norestart /L*v `"$mspLog`""
|
||||
Write-SetupLog " msiexec.exe $mspArgs"
|
||||
|
||||
$psi.Arguments = $mspArgs
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
$proc.WaitForExit()
|
||||
$mspExit = $proc.ExitCode
|
||||
Write-SetupLog " msiexec exit code: $mspExit"
|
||||
|
||||
if ($mspExit -ne 0 -and $mspExit -ne 3010) {
|
||||
Write-SetupLog "ERROR: SP1 patch failed (exit $mspExit). See $mspLog"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Step 3: Deploy profiles, keymaps, menus, accessories ---
|
||||
# Source layout (bundled with this script):
|
||||
# $SourceDir\Profile\*.hep
|
||||
# $SourceDir\Accessories\EB\*.eb*
|
||||
# $SourceDir\HostExplorer\Keymap\*.kmv
|
||||
# $SourceDir\HostExplorer\Menu\*.hmv
|
||||
#
|
||||
# Target layouts (Hummingbird-canonical):
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\Profile\
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\Accessories\EB\
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\HostExplorer\Keymap\
|
||||
# %ProgramData%\Hummingbird\Connectivity\15.00\Shared\HostExplorer\Menu\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\Profile\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\Accessories\EB\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\HostExplorer\Keymap\
|
||||
# <user>\AppData\Roaming\Hummingbird\Connectivity\15.00\HostExplorer\Menu\
|
||||
#
|
||||
# We deploy to ProgramData\Shared (system-wide fallback), Default User (template
|
||||
# inherited by every NEW user profile), and every existing user profile (so
|
||||
# already-created accounts like SupportUser get them immediately).
|
||||
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 3: Deploying profiles/keymaps/menus/macros..."
|
||||
|
||||
# --- Resolve exclude lists from site-config.json (falls back to West Jefferson defaults) ---
|
||||
$siteConfig = Get-SiteConfig
|
||||
$profileExcludes = if ($siteConfig -and $siteConfig.opentext -and $siteConfig.opentext.excludeProfiles) {
|
||||
@($siteConfig.opentext.excludeProfiles)
|
||||
} else {
|
||||
@('WJ_Office.hep', 'IBM_qks.hep', 'mmcs.hep') # West Jefferson defaults
|
||||
}
|
||||
$shortcutExcludes = if ($siteConfig -and $siteConfig.opentext -and $siteConfig.opentext.excludeShortcuts) {
|
||||
@($siteConfig.opentext.excludeShortcuts)
|
||||
} else {
|
||||
@('WJ_Office.lnk', 'IBM_qks.lnk', 'mmcs.lnk')
|
||||
}
|
||||
if ($siteConfig) {
|
||||
Write-SetupLog "Site config loaded - profile excludes: $($profileExcludes -join ', ')"
|
||||
Write-SetupLog "Site config loaded - shortcut excludes: $($shortcutExcludes -join ', ')"
|
||||
} else {
|
||||
Write-SetupLog "No site-config.json found - using West Jefferson defaults for excludes"
|
||||
}
|
||||
|
||||
# Map of source subdir -> destination subdir relative to the Hummingbird root.
|
||||
# Optional Exclude list drops specific filenames from both the source-to-dest
|
||||
# copy AND from the destination if they were left over from a prior install.
|
||||
$contentMap = @(
|
||||
@{
|
||||
Src = 'Profile'
|
||||
Dst = 'Profile'
|
||||
Exclude = $profileExcludes
|
||||
}
|
||||
@{ Src = 'Accessories\EB'; Dst = 'Accessories\EB' }
|
||||
@{ Src = 'HostExplorer\Keymap'; Dst = 'HostExplorer\Keymap' }
|
||||
@{ Src = 'HostExplorer\Menu'; Dst = 'HostExplorer\Menu' }
|
||||
)
|
||||
|
||||
function Copy-HummingbirdContent {
|
||||
param(
|
||||
[string]$RootDst, # Hummingbird root, e.g. C:\ProgramData\Hummingbird\Connectivity\15.00\Shared
|
||||
[string]$Label
|
||||
)
|
||||
foreach ($entry in $contentMap) {
|
||||
$srcPath = Join-Path $SourceDir $entry.Src
|
||||
if (-not (Test-Path $srcPath)) { continue }
|
||||
$dstPath = Join-Path $RootDst $entry.Dst
|
||||
New-Item -Path $dstPath -ItemType Directory -Force | Out-Null
|
||||
|
||||
# Remove any previously-deployed excluded files from the destination
|
||||
# - handles the case where a PC got them from an older install.
|
||||
if ($entry.Exclude) {
|
||||
foreach ($name in $entry.Exclude) {
|
||||
$stale = Join-Path $dstPath $name
|
||||
if (Test-Path -LiteralPath $stale) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $stale -Force -ErrorAction Stop
|
||||
Write-SetupLog " $Label : removed stale $name"
|
||||
} catch {
|
||||
Write-SetupLog " $Label : failed to remove $stale : $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$files = Get-ChildItem -Path $srcPath -File -ErrorAction SilentlyContinue
|
||||
if ($entry.Exclude) {
|
||||
$files = @($files | Where-Object { $entry.Exclude -notcontains $_.Name })
|
||||
}
|
||||
foreach ($f in $files) {
|
||||
Copy-Item -Path $f.FullName -Destination $dstPath -Force
|
||||
}
|
||||
Write-SetupLog " $Label : $($entry.Src) -> $dstPath ($($files.Count) files)"
|
||||
}
|
||||
}
|
||||
|
||||
# 3a. ProgramData Shared
|
||||
$sharedRoot = Join-Path $env:ProgramData 'Hummingbird\Connectivity\15.00\Shared'
|
||||
Copy-HummingbirdContent -RootDst $sharedRoot -Label 'Shared'
|
||||
|
||||
# 3b. Default User (template for new user profiles)
|
||||
$defaultUserRoot = 'C:\Users\Default\AppData\Roaming\Hummingbird\Connectivity\15.00'
|
||||
Copy-HummingbirdContent -RootDst $defaultUserRoot -Label 'Default User'
|
||||
|
||||
# 3c. Every existing user profile under C:\Users\
|
||||
$skipNames = @('Default', 'Default User', 'Public', 'defaultuser0', 'All Users', 'WDAGUtilityAccount')
|
||||
$userDirs = Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue |
|
||||
Where-Object { $skipNames -notcontains $_.Name -and (Test-Path "$($_.FullName)\AppData\Roaming") }
|
||||
|
||||
foreach ($u in $userDirs) {
|
||||
$userRoot = Join-Path $u.FullName 'AppData\Roaming\Hummingbird\Connectivity\15.00'
|
||||
Copy-HummingbirdContent -RootDst $userRoot -Label $u.Name
|
||||
}
|
||||
|
||||
# --- Step 4: Public Desktop shortcuts ---
|
||||
# Uses $shortcutExcludes (resolved from site-config.json above) to skip
|
||||
# deploying unwanted .lnk files AND remove any a prior install left behind.
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 4: Deploying public desktop shortcuts..."
|
||||
$shortcutSrc = Join-Path $SourceDir 'W10shortcuts'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
|
||||
# Clean up stale copies from prior installs first
|
||||
foreach ($name in $shortcutExcludes) {
|
||||
$stale = Join-Path $publicDesktop $name
|
||||
if (Test-Path -LiteralPath $stale) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $stale -Force -ErrorAction Stop
|
||||
Write-SetupLog " removed stale desktop shortcut: $name"
|
||||
} catch {
|
||||
Write-SetupLog " failed to remove stale $stale : $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path $shortcutSrc) {
|
||||
$lnkFiles = Get-ChildItem -Path $shortcutSrc -Filter '*.lnk' -File -ErrorAction SilentlyContinue
|
||||
foreach ($l in $lnkFiles) {
|
||||
if ($shortcutExcludes -contains $l.Name) {
|
||||
Write-SetupLog " skip (excluded): $($l.Name)"
|
||||
continue
|
||||
}
|
||||
Copy-Item -Path $l.FullName -Destination $publicDesktop -Force
|
||||
Write-SetupLog " $($l.Name) -> $publicDesktop"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Step 5: Write registry marker ---
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "Step 5: Writing registry marker..."
|
||||
if (-not (Test-Path $markerKey)) {
|
||||
New-Item -Path $markerKey -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $markerKey -Name $markerVal -Value $expectedVersion -Force
|
||||
Set-ItemProperty -Path $markerKey -Name 'InstalledAt' -Value (Get-Date -Format 'o') -Force
|
||||
|
||||
Write-SetupLog ""
|
||||
Write-SetupLog "================================================================"
|
||||
Write-SetupLog "=== OpenText HostExplorer ShopFloor $expectedVersion deployed ==="
|
||||
Write-SetupLog "================================================================"
|
||||
exit 0
|
||||
@@ -1 +0,0 @@
|
||||
15.0.SP1.2
|
||||
@@ -1,113 +0,0 @@
|
||||
@echo off
|
||||
REM Install-Oracle11r2.cmd
|
||||
REM Expands the GE Oracle Client 11.2 Administrator zip to a temp dir and
|
||||
REM runs Oracle Universal Installer silently with the GE-customized
|
||||
REM response file.
|
||||
REM
|
||||
REM Accepted zip locations (first hit wins):
|
||||
REM %~dp0Oracle_OracleDatabase_11r2_V03.zip (preinstall flat)
|
||||
REM %~dp0..\apps\Oracle_OracleDatabase_11r2_V03.zip (SFLD share, enforcer)
|
||||
REM
|
||||
REM Called by:
|
||||
REM - preinstall runner (Type=EXE, image build time)
|
||||
REM - Install-FromManifest.ps1 (Type=CMD, runtime enforcer)
|
||||
REM
|
||||
REM Oracle 11.2 OUI exit codes worth knowing:
|
||||
REM 0 = success
|
||||
REM 3 = success but with warnings
|
||||
REM 1 = general failure
|
||||
REM 6 = silent install requested but missing / bad response file
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
set "LOG=C:\Logs\OracleClient\install.log"
|
||||
if not exist "C:\Logs\OracleClient" mkdir "C:\Logs\OracleClient"
|
||||
|
||||
REM Emit a datestamp
|
||||
for /f "tokens=2 delims==" %%I in ('wmic os get localdatetime /value 2^>nul ^| find "="') do set LDT=%%I
|
||||
set "STAMP=!LDT:~0,14!"
|
||||
|
||||
echo [%STAMP%] Install-Oracle11r2.cmd starting >> "%LOG%"
|
||||
|
||||
set "SRC_ZIP=%~dp0Oracle_OracleDatabase_11r2_V03.zip"
|
||||
if not exist "%SRC_ZIP%" set "SRC_ZIP=%~dp0..\apps\Oracle_OracleDatabase_11r2_V03.zip"
|
||||
set "STAGING=%TEMP%\oracle-11r2-install"
|
||||
set "CLIENT_DIR=%STAGING%\Oracle_OracleDatabase_11r2_V03\client"
|
||||
set "RSP=%CLIENT_DIR%\response\ge_client_install.rsp"
|
||||
|
||||
if not exist "%SRC_ZIP%" (
|
||||
echo [%STAMP%] ERROR: zip not found at %SRC_ZIP% >> "%LOG%"
|
||||
echo ERROR: zip not found at %SRC_ZIP%
|
||||
exit /b 2
|
||||
)
|
||||
|
||||
echo [%STAMP%] Expanding %SRC_ZIP% to %STAGING% >> "%LOG%"
|
||||
if exist "%STAGING%" rmdir /s /q "%STAGING%" >nul 2>&1
|
||||
mkdir "%STAGING%"
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command ^
|
||||
"try { Expand-Archive -Path '%SRC_ZIP%' -DestinationPath '%STAGING%' -Force -ErrorAction Stop; exit 0 } catch { Write-Error $_; exit 1 }" ^
|
||||
>> "%LOG%" 2>&1
|
||||
|
||||
if not exist "%CLIENT_DIR%\setup.exe" (
|
||||
echo [%STAMP%] ERROR: expanded setup.exe not found at %CLIENT_DIR%\setup.exe >> "%LOG%"
|
||||
exit /b 3
|
||||
)
|
||||
|
||||
if not exist "%RSP%" (
|
||||
echo [%STAMP%] ERROR: response file missing at %RSP% >> "%LOG%"
|
||||
exit /b 4
|
||||
)
|
||||
|
||||
echo [%STAMP%] Running OUI silent install (this takes 2-8 minutes) >> "%LOG%"
|
||||
"%CLIENT_DIR%\setup.exe" -silent -waitforcompletion -nowait ^
|
||||
-ignoreSysPrereqs ^
|
||||
-responseFile "%RSP%" >> "%LOG%" 2>&1
|
||||
set RC=%ERRORLEVEL%
|
||||
|
||||
echo [%STAMP%] OUI exit code: %RC% >> "%LOG%"
|
||||
|
||||
REM --- Drop GE-customized tnsnames.ora / sqlnet.ora / ldap.ora into ORACLE_HOME.
|
||||
REM The zip ships these in client\ora\ but Oracle's setup.exe does not consume
|
||||
REM them - that copy is the GE Wise wrapper's job, which we bypass. Without
|
||||
REM these, every Oracle-dependent app (eDNC/UDC/NTLARS/CMM tooling) fails with
|
||||
REM ORA-12154 "TNS could not resolve the connect identifier specified".
|
||||
REM
|
||||
REM Per-file override: if %~dp0<name>.ora exists (deployed alongside this
|
||||
REM wrapper on the PXE server / SFLD share), use it instead of the bundled
|
||||
REM copy. Lets us push an updated tnsnames.ora without repackaging the zip.
|
||||
REM
|
||||
REM Only run when OUI succeeded (RC 0 or 3). On failure ORACLE_HOME may not
|
||||
REM be fully populated and the copy targets may not exist.
|
||||
if %RC%==0 goto :do_ora_copy
|
||||
if %RC%==3 goto :do_ora_copy
|
||||
goto :skip_ora_copy
|
||||
|
||||
:do_ora_copy
|
||||
set "ORA_DST=C:\Apps\product\11.2.0\client_1\network\admin"
|
||||
if not exist "%ORA_DST%" (
|
||||
echo [%STAMP%] WARN: %ORA_DST% does not exist after OUI - skipping .ora copy >> "%LOG%"
|
||||
goto :skip_ora_copy
|
||||
)
|
||||
for %%F in (tnsnames.ora sqlnet.ora ldap.ora) do (
|
||||
set "ORA_SRC=%~dp0%%F"
|
||||
if not exist "!ORA_SRC!" set "ORA_SRC=%CLIENT_DIR%\ora\%%F"
|
||||
if exist "!ORA_SRC!" (
|
||||
echo [%STAMP%] Copying %%F from !ORA_SRC! to %ORA_DST%\%%F >> "%LOG%"
|
||||
copy /Y "!ORA_SRC!" "%ORA_DST%\%%F" >> "%LOG%" 2>&1
|
||||
) else (
|
||||
echo [%STAMP%] WARN: %%F not found in either %~dp0 or %CLIENT_DIR%\ora >> "%LOG%"
|
||||
)
|
||||
)
|
||||
|
||||
:skip_ora_copy
|
||||
|
||||
REM Cleanup staging dir to reclaim ~1.5 GB - OUI copies everything to ORACLE_HOME
|
||||
echo [%STAMP%] Cleaning up staging dir >> "%LOG%"
|
||||
rmdir /s /q "%STAGING%" >nul 2>&1
|
||||
|
||||
REM OUI returns 0 for success, 3 for success-with-warnings. Treat both as OK.
|
||||
if %RC%==3 (
|
||||
echo [%STAMP%] OUI reported warnings but install succeeded - returning 0 >> "%LOG%"
|
||||
exit /b 0
|
||||
)
|
||||
|
||||
exit /b %RC%
|
||||
File diff suppressed because it is too large
Load Diff
13
playbook/preinstall/postppkg.json
Normal file
13
playbook/preinstall/postppkg.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"Version": "1.0",
|
||||
"_comment": "Site app list, installed after the post-PPKG reboot (once, as SYSTEM). Empty by default. Add one object per app to Applications and put its installer under pre-install/installers/<folder>/ on the PXE server (enrollment share). See docs/kb/How_To_Deploy_PXE_Server_New_Site.html, 'Adding shopfloor apps'.",
|
||||
"_example": {
|
||||
"Name": "Example App 1.2",
|
||||
"Installer": "exampleapp\\ExampleApp.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{00000000-0000-0000-0000-000000000000}"
|
||||
},
|
||||
"Applications": []
|
||||
}
|
||||
@@ -1,213 +1,13 @@
|
||||
{
|
||||
"Version": "1.0",
|
||||
"Site": "West Jefferson",
|
||||
"Applications": [
|
||||
{
|
||||
"_comment": "PowerShell 7.5.4 - installed BEFORE PPKG via FlatUnattendW10-shopfloor.xml FirstLogonCommand Order 6 (race fix: Intune SetupCredentials Win32App install command starts with pwsh.exe; if PS7 not yet installed when that Win32App fires, it errors with FILE_NOT_FOUND 0x80070002 and IME's GRS retry never re-fires under V3Processor). This entry is a backstop - no-op via ProductCode detection if unattend Order 6 already installed it. PreEnrollment flag is informational; runner does not currently filter on it.",
|
||||
"Name": "PowerShell 7.5.4",
|
||||
"Installer": "powershell7\\PowerShell-7.5.4-win-x64.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart ADD_PATH=1 USE_MU=0 ENABLE_MU=0 DISABLE_TELEMETRY=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{E8159677-ACF8-4D64-9D36-5C36B8BBEA39}",
|
||||
"PreEnrollment": true,
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "Oracle Client 11.2 Administrator - installed first because downstream apps (eDNC/NTLARS/UDC and CMM tooling) link against the Oracle home and fail cold if it's missing. Installer is a .cmd wrapper (Type=EXE is the preinstall runner's shim for non-MSI launchers, same pattern as OpenText Setup-OpenText.cmd). The wrapper expects Oracle_OracleDatabase_11r2_V03.zip (686 MB) staged next to it, unpacks to %TEMP%, runs Oracle Universal Installer silently with ge_client_install.rsp, then cleans up the staging dir. OUI exit 3 is treated as success (warnings-but-ok). Detection via the registered home key; downstream upgrades or version pins are handled by the runtime enforcer's Oracle Client 11.2 manifest entry in common/manifest.json. Scoped to the DNC-bearing PC types (collections, nocollections, partmarker, heattreat) plus CMM, whose metrology tooling links the Oracle home; non-DNC types (Genspect, Keyence, WaxAndTrace, Display, Timeclock, Lab) do not get it.",
|
||||
"Name": "Oracle Client 11.2",
|
||||
"Installer": "oracle\\Install-Oracle11r2.cmd",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "",
|
||||
"LogFile": "C:\\Logs\\OracleClient\\install.log",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Oracle\\KEY_OraClient11g_home1",
|
||||
"DetectionName": "ORACLE_HOME_NAME",
|
||||
"DetectionValue": "OraClient11g_home1",
|
||||
"PCTypes": ["gea-shopfloor-collections", "gea-shopfloor-nocollections", "gea-shopfloor-partmarker", "gea-shopfloor-heattreat", "gea-shopfloor-cmm"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2008 SP1 x86 - the bootstrapper (vcredist2008_x86.exe) ignores /norestart and triggers an immediate Windows reboot when files are in use (per Aaron Stebner's MSDN docs). Fix: install the extracted vc_red.msi directly with REBOOT=ReallySuppress, which IS hard-honored by Windows Installer. msiexec may return 3010 (would-have-rebooted-but-suppressed) but won't actually reboot. cab name 'vc_red.cab' is hardcoded in the MSI's Media table - do not rename.",
|
||||
"Name": "VC++ Redistributable 2008 x86",
|
||||
"Installer": "vcredist/2008/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{9BE518E6-ECC6-35A9-88E4-87755C07200F}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2010 x86 - same fix as 2008. Bootstrapper ignores /norestart; extracted MSI with REBOOT=ReallySuppress does not.",
|
||||
"Name": "VC++ Redistributable 2010 x86",
|
||||
"Installer": "vcredist/2010/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F0C3E5D1-1ADE-321E-8167-68EF0DE699A5}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2012 x86 Minimum Runtime - extracted from vcredist2012_x86.exe Burn bundle. Same REBOOT=ReallySuppress fix.",
|
||||
"Name": "VC++ Redistributable 2012 x86 (Minimum)",
|
||||
"Installer": "vcredist/2012-min/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{BD95A8CD-1D9F-35AD-981A-3E7925026EBB}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"Name": "VC++ Redistributable 2012 x86 (Additional)",
|
||||
"Installer": "vcredist/2012-add/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{B175520C-86A2-35A7-8619-86DC379688B9}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2013 x86 Minimum Runtime - extracted from vcredist2013_x86.exe Burn bundle.",
|
||||
"Name": "VC++ Redistributable 2013 x86 (Minimum)",
|
||||
"Installer": "vcredist/2013-min/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{13A4EE12-23EA-3371-91EE-EFB36DDFFF3E}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"Name": "VC++ Redistributable 2013 x86 (Additional)",
|
||||
"Installer": "vcredist/2013-add/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{F8CFEB22-A2E7-3971-9EDA-4B11EDEFC185}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2013 x64 Minimum Runtime - required by Keyence VR-6000 Series Software. Extracted from the Keyence installer's Windows Installer cache. Same REBOOT=ReallySuppress pattern as the x86 variants.",
|
||||
"Name": "VC++ Redistributable 2013 x64 (Minimum)",
|
||||
"Installer": "vcredist/2013-x64-min/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{A749D8E6-B613-3BE3-8F5F-045C84EBA29B}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2013 x64 Additional Runtime - required by Keyence VR-6000 Series Software. Pairs with the Minimum Runtime above.",
|
||||
"Name": "VC++ Redistributable 2013 x64 (Additional)",
|
||||
"Installer": "vcredist/2013-x64-add/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{929FBD26-9020-399B-9A7A-751D61F0B942}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2010 x64 - required by PC-DMIS 2016/2019 R2 on CMM PCs. PCDLRN.exe links against msvcr100.dll and the VS 2010 MFC DLLs which are only provided by this redistributable. Extracted from the PC-DMIS 2016 bundle's attached container (a1 payload). Silent install: /q /norestart. Detection: Uninstall key under the native x64 hive with fixed product GUID.",
|
||||
"Name": "VC++ Redistributable 2010 x64",
|
||||
"Installer": "vcredist/2010-x64/vcredist_x64.exe",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "/q /norestart",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{1D8E6291-B0D5-35EC-8441-6616F567A0F7}",
|
||||
"DetectionName": "DisplayVersion",
|
||||
"DetectionValue": "10.0.40219",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2012 x64 - required by PC-DMIS 2016/2019 R2. Same rationale as 2010 x64; PC-DMIS links against msvcr110.dll / MFC110. Extracted from the PC-DMIS 2016 bundle's attached container (a2 payload). Detection on the Minimum Runtime GUID (the main redist wrapper installs both Minimum and Additional sub-packages).",
|
||||
"Name": "VC++ Redistributable 2012 x64",
|
||||
"Installer": "vcredist/2012-x64/vcredist_x64.exe",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "/q /norestart",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{5AF4E09F-5C9B-3AAF-B731-544D3DC821DD}",
|
||||
"DetectionName": "DisplayVersion",
|
||||
"DetectionValue": "11.0.51106",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "VC++ 2015-2022 x86 - extracted from vcredist2015_2017_2019_2022_x86.exe Burn bundle. The bundle contains 2022 14.44.35211 plus 8 chained KB updates for older 2015/2017/2019 releases. We install only the 2022 Min+Add MSIs - the CRT v140 ABI is shared across 2015/2017/2019/2022, so the latest pair covers all four versions on Windows 10/11.",
|
||||
"Name": "VC++ Redistributable 2022 x86 (Minimum)",
|
||||
"Installer": "vcredist/2022-min/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{922480B5-CAEB-4B1B-AAA4-9716EFDCE26B}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"Name": "VC++ Redistributable 2022 x86 (Additional)",
|
||||
"Installer": "vcredist/2022-add/installer.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart REBOOT=ReallySuppress NOVSUI=1 USING_EXUIH_SILENT=1",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{C18FB403-1E88-43C8-AD8A-CED50F23DE8B}",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "Adobe Acrobat Reader DC - same version as GEA-Engineering image. Two-step install via Install-AcroReader.cmd wrapper: base MSI with enterprise transform (AcroRead.mst) then DC update patch (AcroRdrDCUpd2500120531.msp). Source files on PXE server at /srv/samba/winpeapps/gea-engineer/Deploy/Applications/extra/adobe/ - copy to /srv/samba/enrollment/preinstall/installers/adobe/.",
|
||||
"Name": "Adobe Acrobat Reader DC",
|
||||
"Installer": "adobe\\Install-AcroReader.cmd",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "",
|
||||
"DetectionMethod": "File",
|
||||
"DetectionPath": "C:\\Program Files (x86)\\Adobe\\Acrobat Reader DC\\Reader\\AcroRd32.exe",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "OpenText HostExplorer 15 SP1 ShopFloor - replaced the WJDT-built OpenText.exe Inno wrapper with our own Setup-OpenText.ps1 that does the same install steps (msiexec base + ShopFloor.mst transform + SP1 .msp patch) PLUS fans the per-user profile/keymap/menu/macro content out to Default User and every existing user profile. The Inno wrapper deployed per-user content to {userappdata} which resolves to whichever user is running the installer (SYSTEM under DSC, single user under PreInstall) so Azure-AD users never saw the profiles. Setup-OpenText.cmd is a tiny launcher that hands off to Setup-OpenText.ps1 because the runner only handles MSI/EXE types. All bundled files live in vcredist-style subtree at dependencies/opentext/ and get xcopied through WinPE staging. NO DetectionMethod here on purpose - Setup-OpenText.ps1 owns its own version check by reading version.txt next to itself and comparing to HKLM:\\SOFTWARE\\GE\\OpenText\\Installed; the runner always invokes the wrapper (cost: ~1s of PowerShell launch when up-to-date) so the version constant lives in exactly one place: dependencies/opentext/version.txt.",
|
||||
"Name": "OpenText HostExplorer ShopFloor",
|
||||
"Installer": "opentext\\Setup-OpenText.cmd",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "",
|
||||
"LogFile": "C:\\Logs\\PreInstall\\Setup-OpenText.log",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "UDC_Setup.exe spawns a hidden WPF window (UDC.exe) after install and never exits, so the runner needs KillAfterDetection: true to terminate UDC_Setup.exe + UDC.exe once the registry detection passes. This is an OPT-IN flag - normal installers should NOT set it because killing msiexec mid-install leaves msiserver holding the install mutex and the next msiexec call returns 1618 (Oracle hit this exact bug).",
|
||||
"Name": "UDC",
|
||||
"Installer": "UDC_Setup.exe",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "\"West Jefferson\" -9999",
|
||||
"KillAfterDetection": true,
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\UDC",
|
||||
"PCTypes": ["gea-shopfloor-collections"],
|
||||
"PCTypesStrict": true,
|
||||
"_pcTypesNote": "UDC = the C in 'collections'. nocollections does NOT collect data so MUST NOT install UDC. PCTypesStrict bypasses the alias-expansion matcher so a nocollections PC's myNames (which transitively contains gea-shopfloor-collections via the Standard group) still won't match this entry."
|
||||
},
|
||||
{
|
||||
"_comment": "Display kiosk app (Lobby Display or Dashboard). Install-KioskApp.cmd wrapper reads C:\\Enrollment\\display-type.txt to determine which installer to run. Both GEAerospaceLobbyDisplaySetup.exe and GEAerospaceDashboardSetup.exe must be staged in the display\\ subtree alongside the wrapper. Inno Setup /VERYSILENT is idempotent so no detection needed.",
|
||||
"Name": "Display Kiosk App",
|
||||
"Installer": "display\\Install-KioskApp.cmd",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "",
|
||||
"LogFile": "C:\\Logs\\PreInstall\\Dashboard-install.log",
|
||||
"PCTypes": ["Display"]
|
||||
},
|
||||
{
|
||||
"_comment": "WJF Defect Tracker - replaces old ClickOnce deployment. MSI creates desktop + Start Menu shortcuts. Enforcer on SFLD share handles version updates post-imaging. TARGETDIR explicit because VS deployment MSI defaults to random path without it.",
|
||||
"Name": "WJF Defect Tracker",
|
||||
"Installer": "WJF_Defect_Tracker.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart ALLUSERS=1 REBOOT=ReallySuppress TARGETDIR=\"C:\\Program Files (x86)\\WJF_Defect_Tracker\"",
|
||||
"DetectionMethod": "File",
|
||||
"DetectionPath": "C:\\Program Files (x86)\\WJF_Defect_Tracker\\Defect_Tracker.exe",
|
||||
"PCTypes": ["*"]
|
||||
},
|
||||
{
|
||||
"_comment": "Shopfloor Standard serial-port drivers: StarTech PCIe serial adapter (MosChip-based) + Prolific PL2303 USB-to-serial. Install-Drivers.cmd runs pnputil /add-driver with /subdirs /install so every bundled INF under drivers/ lands in the Windows driver store and auto-binds to matching hardware present now or plugged in later. Installed on every PC type (PCTypes ['*']) because serial hardware turns up across bays; a serial driver on a PC without the hardware is harmless - it just sits in the driver store until matching hardware is plugged in.",
|
||||
"Name": "Shopfloor Serial Drivers",
|
||||
"Installer": "drivers\\Install-Drivers.cmd",
|
||||
"Type": "EXE",
|
||||
"InstallArgs": "",
|
||||
"LogFile": "C:\\Logs\\PreInstall\\Install-Drivers.log",
|
||||
"DetectionMethod": "File",
|
||||
"DetectionPath": "C:\\ProgramData\\PXEDrivers\\drivers-installed.marker",
|
||||
"PCTypes": ["*"]
|
||||
}
|
||||
]
|
||||
"_comment": "Site app list, installed during setup (same logon as the PPKG, before the post-PPKG reboot). Empty by default. Add one object per app to Applications and put its installer under pre-install/installers/<folder>/ on the PXE server (enrollment share). See docs/kb/How_To_Deploy_PXE_Server_New_Site.html, 'Adding shopfloor apps'.",
|
||||
"_example": {
|
||||
"Name": "Example App 1.2",
|
||||
"Installer": "exampleapp\\ExampleApp.msi",
|
||||
"Type": "MSI",
|
||||
"InstallArgs": "/qn /norestart",
|
||||
"DetectionMethod": "Registry",
|
||||
"DetectionPath": "HKLM:\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\{00000000-0000-0000-0000-000000000000}"
|
||||
},
|
||||
"Applications": []
|
||||
}
|
||||
|
||||
18
playbook/pxe-server-helpers/dnsmasq.service.d/nic-guard.conf
Normal file
18
playbook/pxe-server-helpers/dnsmasq.service.d/nic-guard.conf
Normal file
@@ -0,0 +1,18 @@
|
||||
# dnsmasq drop-in: refuse to start DHCP/PXE unless the imaging NIC is on
|
||||
# the isolated imaging network.
|
||||
#
|
||||
# Installed as /etc/systemd/system/dnsmasq.service.d/nic-guard.conf by
|
||||
# pxe_server_setup.yml. pxe-nic-check --guard listens on the imaging NIC
|
||||
# for 5 s and sends one DHCPDISCOVER (~6 s total), then exits:
|
||||
# 0 safe to serve (isolated, no link, or the check itself broke - it
|
||||
# fails open on bugs and says so loudly in the journal)
|
||||
# 1 DO NOT serve: a foreign DHCP server or a managed/corporate switch
|
||||
# (LLDP, CDP, 802.1X) is on the imaging NIC. dnsmasq does not start.
|
||||
#
|
||||
# Why did dnsmasq not start? journalctl -u dnsmasq -b | grep pxe-nic-check
|
||||
# Full picture: pxe-nic-check (or the webapp status page)
|
||||
# Guard does not run on "systemctl reload"; every start/restart runs it,
|
||||
# including the @reboot cron restart.
|
||||
|
||||
[Service]
|
||||
ExecStartPre=/usr/local/sbin/pxe-nic-check --guard
|
||||
@@ -3,10 +3,15 @@
|
||||
# pxe-dhcp-hook.sh - dnsmasq dhcp-script hook.
|
||||
#
|
||||
# Runs every time a PXE client gets/changes/releases a DHCP lease on
|
||||
# 172.16.9.0/24. Flushes conntrack entries and drops any lingering
|
||||
# TCP sockets for that client IP. Prevents stale server-side state from
|
||||
# causing "System error 53 - network path not found" when a WinPE client
|
||||
# re-images the same machine without a clean SMB session teardown.
|
||||
# 172.16.9.0/24. Flushes that client's SMB (TCP 445 and 139) conntrack
|
||||
# entries and drops its lingering SMB sockets. Prevents stale server-side
|
||||
# state from causing "System error 53 - network path not found" when a
|
||||
# WinPE client re-images the same machine without a clean SMB session
|
||||
# teardown.
|
||||
#
|
||||
# SMB only, on purpose: the lease that fires this hook is iPXE's, and its
|
||||
# first HTTP fetch of the boot menu is racing this script. Killing every
|
||||
# socket or conntrack entry for the client IP broke that fetch.
|
||||
#
|
||||
# dnsmasq calls this with argv:
|
||||
# $1 = action (add | old | del)
|
||||
@@ -14,10 +19,21 @@
|
||||
# $3 = client IP
|
||||
# $4 = hostname (may be empty)
|
||||
#
|
||||
# A re-image usually comes back to the SAME lease (same MAC, same IP), which
|
||||
# dnsmasq reports as "old", not "add" - and only at all because dnsmasq.conf
|
||||
# sets script-on-renewal. "old" also fires when a running Windows PC renews
|
||||
# its lease, so on "old" we only clear state when the request came from PXE
|
||||
# firmware or iPXE (vendor class PXEClient...): that machine has just
|
||||
# rebooted into a network boot, so any server-side session for its IP is
|
||||
# dead. A renewal from Windows (vendor class "MSFT 5.0") is left alone.
|
||||
#
|
||||
# Must exit quickly; dnsmasq waits for it. Logs to syslog via logger.
|
||||
|
||||
set +e
|
||||
|
||||
# dnsmasq's environment may lack sbin; keep any PATH it gave us first.
|
||||
PATH="${PATH:+$PATH:}/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
action="$1"
|
||||
mac="$2"
|
||||
ip="$3"
|
||||
@@ -25,13 +41,26 @@ host="${4:-}"
|
||||
|
||||
[ -z "$ip" ] && exit 0
|
||||
|
||||
clear_state() {
|
||||
local port
|
||||
for port in 445 139; do
|
||||
# client -> our share, and anything the other way round
|
||||
conntrack -D -p tcp -s "$ip" --dport "$port" >/dev/null 2>&1
|
||||
conntrack -D -p tcp -d "$ip" --sport "$port" >/dev/null 2>&1
|
||||
done
|
||||
# our SMB sockets (local port 445/139) whose peer is this client
|
||||
ss -K -t "( sport = :445 or sport = :139 ) and dst = $ip" >/dev/null 2>&1
|
||||
logger -t pxe-dhcp-hook "cleared stale SMB state for $1 $ip ($mac${host:+ $host})"
|
||||
}
|
||||
|
||||
case "$action" in
|
||||
add|del)
|
||||
/usr/sbin/conntrack -D -s "$ip" >/dev/null 2>&1
|
||||
/usr/sbin/conntrack -D -d "$ip" >/dev/null 2>&1
|
||||
/usr/bin/ss -K "dst = $ip" >/dev/null 2>&1
|
||||
/usr/bin/ss -K "src = $ip" >/dev/null 2>&1
|
||||
logger -t pxe-dhcp-hook "cleared stale state for $action $ip ($mac${host:+ $host})"
|
||||
clear_state "$action"
|
||||
;;
|
||||
old)
|
||||
case "${DNSMASQ_VENDOR_CLASS:-}" in
|
||||
PXEClient*) clear_state "old (network boot, ${DNSMASQ_VENDOR_CLASS%%:UNDI*})" ;;
|
||||
esac
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
516
playbook/pxe-server-helpers/pxe-netplan-apply.sh
Executable file
516
playbook/pxe-server-helpers/pxe-netplan-apply.sh
Executable file
@@ -0,0 +1,516 @@
|
||||
#!/bin/bash
|
||||
# pxe-netplan-apply.sh - install the PXE server's netplan config without
|
||||
# ever leaving the box unreachable on the corporate side.
|
||||
#
|
||||
# Usage:
|
||||
# pxe-netplan-apply.sh STAGED_YAML PXE_NAME PXE_MAC [CORP_NAME CORP_MAC]
|
||||
# pxe-netplan-apply.sh --preflight STAGED_YAML PXE_NAME PXE_MAC [CORP_NAME CORP_MAC]
|
||||
# pxe-netplan-apply.sh --revert BACKUP_DIR (the auto-revert timer)
|
||||
#
|
||||
# Without CORP_NAME/CORP_MAC (corp_nic_mac empty in the playbook) only the
|
||||
# imaging NIC is pinned and renamed; the other wired ports are catch-all
|
||||
# DHCP clients that keep their names. Every pre-flight refusal about the
|
||||
# imaging NIC applies the same.
|
||||
#
|
||||
# Env (set by pxe_server_setup.yml):
|
||||
# PXE_SUBNET imaging LAN, e.g. 172.16.9.0/24 (SSH peers inside it may
|
||||
# legitimately come in on the imaging NIC)
|
||||
# PXE_SSH_CLIENT IP of the admin's SSH client ($SSH_CONNECTION field 1);
|
||||
# every established sshd peer is checked as well
|
||||
# PXE_NETPLAN_REVERT_AFTER seconds before the auto-revert fires (120)
|
||||
# PXE_NETPLAN_CORP_WAIT seconds to wait for the corp IPv4 again (30)
|
||||
#
|
||||
# Steps (apply):
|
||||
# 1. validate STAGED_YAML in a scratch root (netplan generate --root-dir);
|
||||
# nothing on the box changes if it is invalid
|
||||
# 2. stop here (UNCHANGED) if it is already installed, no other file
|
||||
# configures wired NICs, and both NICs carry their stable names
|
||||
# 3. pre-flight: REFUSE (exit 1, nothing changed) when the NIC that
|
||||
# PXE_MAC names holds the default route, a DHCP lease, or the route to
|
||||
# an SSH client outside PXE_SUBNET. That NIC becomes static with no
|
||||
# gateway, so any of these means it is the corp-cabled one (swapped
|
||||
# MACs) and applying would cut the admin off.
|
||||
# 4. back up /etc/netplan to /var/backups/netplan-<timestamp>/, record the
|
||||
# current NIC names, and arm an auto-revert (systemd-run timer) that
|
||||
# restores the backup unless step 8 cancels it
|
||||
# 5. install it (other wired configs moved aside), netplan generate
|
||||
# 6. rename the NICs to their stable names (the .link files netplan
|
||||
# writes do it on every later boot)
|
||||
# 7. netplan apply
|
||||
# 8. check: both names present, the imaging NIC has its IPv4, the corp
|
||||
# NIC has IPv4 again if it had one before (PXE_NETPLAN_CORP_WAIT s),
|
||||
# SSH clients still route out of the corp side; then cancel the revert
|
||||
# Any failure in 5-8 restores the backup AND the previous NIC names, then
|
||||
# exits 1. Prints CHANGED / UNCHANGED (or NEEDED for --preflight) as the
|
||||
# last line.
|
||||
|
||||
set -u
|
||||
|
||||
NETPLAN_DIR=${NETPLAN_DIR:-/etc/netplan}
|
||||
SYSFS_NET=${SYSFS_NET:-/sys/class/net}
|
||||
BACKUP_ROOT=${BACKUP_ROOT:-/var/backups}
|
||||
LEASE_DIR=${LEASE_DIR:-/run/systemd/netif/leases}
|
||||
LOCK_FILE=${LOCK_FILE:-/run/pxe-netplan-apply.lock}
|
||||
REVERT_AFTER=${PXE_NETPLAN_REVERT_AFTER:-120}
|
||||
CORP_WAIT=${PXE_NETPLAN_CORP_WAIT:-30}
|
||||
PXE_WAIT=${PXE_NETPLAN_PXE_WAIT:-20}
|
||||
TARGET=$NETPLAN_DIR/50-cloud-init.yaml
|
||||
SELF=$(readlink -f "$0")
|
||||
|
||||
log() { echo "pxe-netplan-apply: $*"; }
|
||||
|
||||
# --- NIC helpers -------------------------------------------------------------
|
||||
|
||||
# Name of the physical NIC that has MAC $1 (empty if none).
|
||||
iface_of() {
|
||||
local d
|
||||
for d in "$SYSFS_NET"/*; do
|
||||
[ -e "$d/device" ] || continue
|
||||
if [ "$(cat "$d/address" 2>/dev/null)" = "$1" ]; then
|
||||
basename "$d"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
has_v4() {
|
||||
ip -4 -o addr show dev "$1" scope global 2>/dev/null | grep -q ' inet '
|
||||
}
|
||||
|
||||
default_route_devs() {
|
||||
{ ip -4 route show default; ip -6 route show default; } 2>/dev/null |
|
||||
awk '{for (i = 1; i < NF; i++) if ($i == "dev") print $(i + 1)}' | sort -u
|
||||
}
|
||||
|
||||
# DHCP-assigned address (networkd and NetworkManager both flag those
|
||||
# "dynamic"), or a networkd lease file for the link.
|
||||
has_dhcp_lease() {
|
||||
local idx
|
||||
ip -o addr show dev "$1" 2>/dev/null | grep -qw dynamic && return 0
|
||||
idx=$(cat "$SYSFS_NET/$1/ifindex" 2>/dev/null)
|
||||
[ -n "$idx" ] && [ -s "$LEASE_DIR/$idx" ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
route_dev() {
|
||||
ip -o route get "$1" 2>/dev/null |
|
||||
awk '{for (i = 1; i < NF; i++) if ($i == "dev") {print $(i + 1); exit}}'
|
||||
}
|
||||
|
||||
ip2int() {
|
||||
local a b c d
|
||||
[[ "$1" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)\.([0-9]+)$ ]] || return 1
|
||||
a=${BASH_REMATCH[1]} b=${BASH_REMATCH[2]} c=${BASH_REMATCH[3]} d=${BASH_REMATCH[4]}
|
||||
echo $(( (a << 24) | (b << 16) | (c << 8) | d ))
|
||||
}
|
||||
|
||||
in_subnet() {
|
||||
local ipn netn len mask
|
||||
[ -n "${2:-}" ] || return 1
|
||||
ipn=$(ip2int "$1") || return 1
|
||||
netn=$(ip2int "${2%/*}") || return 1
|
||||
len=${2#*/}
|
||||
[[ "$len" =~ ^[0-9]+$ ]] && [ "$len" -le 32 ] || return 1
|
||||
mask=$(( len == 0 ? 0 : (0xFFFFFFFF << (32 - len)) & 0xFFFFFFFF ))
|
||||
[ $(( ipn & mask )) -eq $(( netn & mask )) ]
|
||||
}
|
||||
|
||||
# Admin SSH clients: PXE_SSH_CLIENT, our own SSH_CONNECTION, and every
|
||||
# established peer of sshd (port 22). IPv4-mapped IPv6 is unwrapped.
|
||||
ssh_peers() {
|
||||
{
|
||||
[ -n "${PXE_SSH_CLIENT:-}" ] && echo "$PXE_SSH_CLIENT"
|
||||
[ -n "${SSH_CONNECTION:-}" ] && echo "${SSH_CONNECTION%% *}"
|
||||
ss -Htn state established '( sport = :22 )' 2>/dev/null | awk '{print $NF}' |
|
||||
sed -E 's/:[0-9]+$//; s/^\[//; s/\]$//'
|
||||
} | sed -E 's/^::ffff://' | grep -E '^[0-9a-fA-F.:]+$' | sort -u
|
||||
}
|
||||
|
||||
# --- pre-flight --------------------------------------------------------------
|
||||
|
||||
# 0 = safe to apply. Prints every reason it is not.
|
||||
preflight() {
|
||||
local pif cif peer dev bad=0
|
||||
pif=$(iface_of "$PXE_MAC")
|
||||
if [ -z "$pif" ]; then
|
||||
log "REFUSED: no physical NIC has pxe_nic_mac $PXE_MAC"
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$CORP_MAC" ]; then
|
||||
cif=$(iface_of "$CORP_MAC")
|
||||
if [ -z "$cif" ]; then
|
||||
log "REFUSED: no physical NIC has corp_nic_mac $CORP_MAC"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if default_route_devs | grep -qxF "$pif"; then
|
||||
log "REFUSED: $pif (pxe_nic_mac $PXE_MAC) holds the default route"
|
||||
bad=1
|
||||
fi
|
||||
if has_dhcp_lease "$pif"; then
|
||||
log "REFUSED: $pif (pxe_nic_mac $PXE_MAC) has a DHCP lease"
|
||||
bad=1
|
||||
fi
|
||||
while IFS= read -r peer; do
|
||||
[ -n "$peer" ] || continue
|
||||
dev=$(route_dev "$peer")
|
||||
[ "$dev" = "$pif" ] || continue
|
||||
if in_subnet "$peer" "${PXE_SUBNET:-}"; then
|
||||
continue # an admin on the imaging LAN keeps 172.16.9.1
|
||||
fi
|
||||
log "REFUSED: the SSH client $peer is reached through $pif (pxe_nic_mac $PXE_MAC)"
|
||||
bad=1
|
||||
done < <(ssh_peers)
|
||||
if [ "$bad" -ne 0 ]; then
|
||||
log "The NIC named by pxe_nic_mac looks like the GE corporate one: the MACs are"
|
||||
log "probably swapped, or the cables are. It would become"
|
||||
log "static ${PXE_SUBNET:-imaging} with no gateway and cut this box off. Nothing was changed."
|
||||
log "NICs now: $(for d in "$SYSFS_NET"/*; do [ -e "$d/device" ] && printf '%s=%s ' "$(basename "$d")" "$(cat "$d/address" 2>/dev/null)"; done)"
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- renaming ----------------------------------------------------------------
|
||||
|
||||
link_rename() {
|
||||
local cur=$1 new=$2
|
||||
log "renaming $cur to $new"
|
||||
if ip link set dev "$cur" down &&
|
||||
ip link set dev "$cur" name "$new" &&
|
||||
ip link set dev "$new" up; then
|
||||
return 0
|
||||
fi
|
||||
log "ERROR: rename $cur -> $new failed"
|
||||
ip link set dev "$cur" up 2>/dev/null
|
||||
ip link set dev "$new" up 2>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
# set_names MAC1 NAME1 [MAC2 NAME2]: give each MAC its name. A NIC that must
|
||||
# change goes through a temporary name first, so a swap (pxe0 <-> corp0)
|
||||
# works in both directions. Temporary names carry this run's PID, so they
|
||||
# never collide with a name left over from an earlier (failed) pass. A pair
|
||||
# with an empty MAC or name is skipped (no pinned GE NIC).
|
||||
set_names() {
|
||||
local -a macs=() want=() cur=()
|
||||
local i
|
||||
while [ $# -ge 2 ]; do
|
||||
if [ -n "$1" ] && [ -n "$2" ]; then
|
||||
macs+=("$1"); want+=("$2")
|
||||
fi
|
||||
shift 2
|
||||
done
|
||||
RENAME_SEQ=$(( ${RENAME_SEQ:-0} + 1 ))
|
||||
for i in "${!macs[@]}"; do
|
||||
cur[i]=$(iface_of "${macs[i]}")
|
||||
if [ -z "${cur[i]}" ]; then
|
||||
log "ERROR: no NIC with MAC ${macs[i]}"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
for i in "${!macs[@]}"; do
|
||||
[ "${cur[i]}" = "${want[i]}" ] && continue
|
||||
link_rename "${cur[i]}" "pxt$$-$RENAME_SEQ$i" || return 1
|
||||
cur[i]=pxt$$-$RENAME_SEQ$i
|
||||
done
|
||||
for i in "${!macs[@]}"; do
|
||||
[ "${cur[i]}" = "${want[i]}" ] && continue
|
||||
link_rename "${cur[i]}" "${want[i]}" || return 1
|
||||
done
|
||||
for i in "${!macs[@]}"; do
|
||||
if [ "$(iface_of "${macs[i]}")" != "${want[i]}" ]; then
|
||||
log "ERROR: MAC ${macs[i]} is not named ${want[i]} after renaming"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
names_ok() {
|
||||
[ "$(iface_of "$PXE_MAC")" = "$PXE_NAME" ] || return 1
|
||||
if [ -n "$CORP_MAC" ]; then
|
||||
[ "$(iface_of "$CORP_MAC")" = "$CORP_NAME" ]
|
||||
else
|
||||
[ -z "$(stray_names)" ]
|
||||
fi
|
||||
}
|
||||
|
||||
# No pinned GE NIC: "MAC NAME" for every other physical NIC whose name the
|
||||
# catch-all (en*, eth*) would not match - the previous imaging NIC still
|
||||
# called pxe0 after a re-pick, or corp0 from a pinned config. Each gets the
|
||||
# kernel-style MAC name enx<mac>, so it becomes a catch-all DHCP client and
|
||||
# pxe0 is free for the new imaging NIC.
|
||||
stray_names() {
|
||||
local d n m
|
||||
[ -z "$CORP_MAC" ] || return 0
|
||||
for d in "$SYSFS_NET"/*; do
|
||||
[ -e "$d/device" ] || continue
|
||||
n=${d##*/}
|
||||
m=$(cat "$d/address" 2>/dev/null)
|
||||
[ "$m" = "$PXE_MAC" ] && continue
|
||||
[ "$(cat "$d/type" 2>/dev/null)" = 1 ] || continue
|
||||
[ -e "$d/wireless" ] || [ -e "$d/phy80211" ] || [ -e "$d/bridge" ] && continue
|
||||
case "$n" in en*|eth*) continue ;; esac
|
||||
echo "$m enx${m//:/}"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# MACs of the physical NICs other than the imaging one that hold a DHCP
|
||||
# (dynamic) IPv4: the GE side. They must have one again after the apply,
|
||||
# or an admin on the GE side may have been cut off. Static addresses (the
|
||||
# previous imaging NIC's 172.16.9.1) do not count. By MAC: names change.
|
||||
other_v4_macs() {
|
||||
local d n m
|
||||
for d in "$SYSFS_NET"/*; do
|
||||
[ -e "$d/device" ] || continue
|
||||
n=${d##*/}
|
||||
m=$(cat "$d/address" 2>/dev/null)
|
||||
[ "$m" = "$PXE_MAC" ] && continue
|
||||
ip -4 -o addr show dev "$n" scope global dynamic 2>/dev/null | grep -q ' inet ' && echo "$m"
|
||||
done
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- backup / restore --------------------------------------------------------
|
||||
|
||||
# Other netplan files that configure wired NICs (installer leftovers, an
|
||||
# en* DHCP match): they would put a second config on the two NICs. Files
|
||||
# without "ethernets" (WiFi, NetworkManager wifi profiles) are left alone.
|
||||
other_files() {
|
||||
find "$NETPLAN_DIR" -maxdepth 1 -type f \( -name '*.yaml' -o -name '*.yml' \) \
|
||||
! -path "$TARGET" -exec grep -l 'ethernets' {} + 2>/dev/null | sort
|
||||
}
|
||||
|
||||
# State of one apply, next to (not inside) the backup, so restoring the
|
||||
# backup never copies it into /etc/netplan.
|
||||
write_state() {
|
||||
{
|
||||
echo "NETPLAN_DIR=$NETPLAN_DIR"
|
||||
echo "PXE_MAC=$PXE_MAC"
|
||||
echo "ORIG_PXE_IF=$ORIG_PXE_IF"
|
||||
echo "CORP_MAC=$CORP_MAC"
|
||||
echo "ORIG_CORP_IF=$ORIG_CORP_IF"
|
||||
echo "EXTRA=$EXTRA"
|
||||
} >"$BACKUP.state"
|
||||
}
|
||||
|
||||
read_state() {
|
||||
local k v
|
||||
while IFS='=' read -r k v; do
|
||||
case "$k" in
|
||||
NETPLAN_DIR) [[ "$v" =~ ^/[A-Za-z0-9/._-]*$ ]] && NETPLAN_DIR=$v ;;
|
||||
PXE_MAC|CORP_MAC) [[ "$v" =~ ^([0-9a-f]{2}:){5}[0-9a-f]{2}$ ]] && printf -v "$k" '%s' "$v" ;;
|
||||
ORIG_PXE_IF|ORIG_CORP_IF) [[ "$v" =~ ^[A-Za-z0-9._-]{0,15}$ ]] && printf -v "$k" '%s' "$v" ;;
|
||||
EXTRA) [[ "$v" =~ ^(([0-9a-f]{2}:){5}[0-9a-f]{2}=[A-Za-z0-9._-]{1,15},?)*$ ]] && EXTRA=$v ;;
|
||||
esac
|
||||
done <"$BACKUP.state"
|
||||
TARGET=$NETPLAN_DIR/50-cloud-init.yaml
|
||||
}
|
||||
|
||||
cancel_revert() {
|
||||
[ -n "${REVERT_UNIT:-}" ] || return 0
|
||||
systemctl stop "$REVERT_UNIT.timer" "$REVERT_UNIT.service" >/dev/null 2>&1
|
||||
if systemctl is-active --quiet "$REVERT_UNIT.timer" 2>/dev/null; then
|
||||
log "WARNING: could not cancel $REVERT_UNIT.timer (the committed marker still stops it)"
|
||||
fi
|
||||
}
|
||||
|
||||
# Put the backed-up config AND the previous NIC names back.
|
||||
restore() {
|
||||
local rc=0
|
||||
log "restoring the previous config from $BACKUP"
|
||||
find "$NETPLAN_DIR" -maxdepth 1 -type f \( -name '*.yaml' -o -name '*.yml' \) -delete
|
||||
cp -a "$BACKUP/." "$NETPLAN_DIR/"
|
||||
if [ -n "${ORIG_PXE_IF:-}" ]; then
|
||||
local -a back=()
|
||||
local pair
|
||||
for pair in ${EXTRA//,/ }; do back+=("${pair%%=*}" "${pair#*=}"); done
|
||||
set_names "$PXE_MAC" "$ORIG_PXE_IF" "$CORP_MAC" "${ORIG_CORP_IF:-}" "${back[@]}" ||
|
||||
{ log "ERROR: could not put the previous NIC names back"; rc=1; }
|
||||
fi
|
||||
netplan generate && netplan apply || { log "ERROR: netplan apply of the restored config failed"; rc=1; }
|
||||
touch "$BACKUP.reverted"
|
||||
return $rc
|
||||
}
|
||||
|
||||
fail() {
|
||||
log "ERROR: $*"
|
||||
restore
|
||||
cancel_revert
|
||||
log "the previous config is back in place; nothing applied"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- modes -------------------------------------------------------------------
|
||||
|
||||
MODE=apply
|
||||
case "${1:-}" in
|
||||
--preflight) MODE=preflight; shift ;;
|
||||
--revert)
|
||||
[ $# -eq 2 ] || { echo "usage: $0 --revert BACKUP_DIR" >&2; exit 2; }
|
||||
BACKUP=${2%/}
|
||||
[ -d "$BACKUP" ] && [ -f "$BACKUP.state" ] || { log "revert: no backup/state at $BACKUP"; exit 1; }
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -w 600 9 || log "revert: lock still held after 600 s; reverting anyway"
|
||||
if [ -e "$BACKUP.committed" ] || [ -e "$BACKUP.reverted" ]; then
|
||||
log "revert: $BACKUP already committed or reverted; nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
PXE_MAC="" CORP_MAC="" ORIG_PXE_IF="" ORIG_CORP_IF="" EXTRA=""
|
||||
read_state
|
||||
log "revert: the apply was not confirmed within its window"
|
||||
restore
|
||||
exit $?
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ $# -ne 3 ] && [ $# -ne 5 ]; then
|
||||
echo "usage: $0 [--preflight] STAGED_YAML PXE_NAME PXE_MAC [CORP_NAME CORP_MAC]" >&2
|
||||
exit 2
|
||||
fi
|
||||
STAGED=$1
|
||||
PXE_NAME=$2
|
||||
PXE_MAC=${3,,}
|
||||
CORP_NAME=${4:-}
|
||||
CORP_MAC=${5:-}
|
||||
CORP_MAC=${CORP_MAC,,}
|
||||
|
||||
# 1. validate in a scratch root
|
||||
SCRATCH=$(mktemp -d)
|
||||
trap 'rm -rf "$SCRATCH"' EXIT
|
||||
mkdir -p "$SCRATCH/etc/netplan"
|
||||
install -m 600 "$STAGED" "$SCRATCH/etc/netplan/50-cloud-init.yaml"
|
||||
if ! netplan generate --root-dir "$SCRATCH"; then
|
||||
log "ERROR: $STAGED does not validate; nothing changed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. already in place?
|
||||
if cmp -s "$STAGED" "$TARGET" && [ -z "$(other_files)" ] && names_ok; then
|
||||
log "netplan already in place, $PXE_NAME${CORP_NAME:+ and $CORP_NAME} present"
|
||||
echo UNCHANGED
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# 3. pre-flight
|
||||
if ! preflight; then
|
||||
exit 1
|
||||
fi
|
||||
if [ "$MODE" = preflight ]; then
|
||||
log "pre-flight passed; the netplan config would change"
|
||||
echo NEEDED
|
||||
exit 0
|
||||
fi
|
||||
|
||||
exec 9>"$LOCK_FILE"
|
||||
flock -w 60 9 || { log "ERROR: another pxe-netplan-apply holds $LOCK_FILE; nothing changed"; exit 1; }
|
||||
|
||||
# 4. back up, record names, arm the auto-revert
|
||||
ORIG_PXE_IF=$(iface_of "$PXE_MAC")
|
||||
ORIG_CORP_IF=""
|
||||
[ -n "$CORP_MAC" ] && ORIG_CORP_IF=$(iface_of "$CORP_MAC")
|
||||
CORP_HAD_V4=0
|
||||
[ -n "$ORIG_CORP_IF" ] && has_v4 "$ORIG_CORP_IF" && CORP_HAD_V4=1
|
||||
OTHERS_V4=""
|
||||
[ -z "$CORP_MAC" ] && OTHERS_V4=$(other_v4_macs)
|
||||
# NICs to move off a name the catch-all would not match, recorded with
|
||||
# their current names for the revert ("mac=name,...").
|
||||
EXTRA=""
|
||||
FREE=()
|
||||
while read -r m newname; do
|
||||
[ -n "$m" ] || continue
|
||||
EXTRA="${EXTRA:+$EXTRA,}$m=$(iface_of "$m")"
|
||||
FREE+=("$m" "$newname")
|
||||
done < <(stray_names)
|
||||
BACKUP=$BACKUP_ROOT/netplan-$(date +%Y%m%d-%H%M%S)
|
||||
mkdir -p "$BACKUP"
|
||||
cp -a "$NETPLAN_DIR/." "$BACKUP/"
|
||||
write_state
|
||||
log "previous config saved in $BACKUP (NICs: $ORIG_PXE_IF${ORIG_CORP_IF:+, $ORIG_CORP_IF})"
|
||||
|
||||
REVERT_UNIT=""
|
||||
if [ "$REVERT_AFTER" -gt 0 ] 2>/dev/null && command -v systemd-run >/dev/null; then
|
||||
REVERT_UNIT=pxe-netplan-revert-$(basename "$BACKUP" | tr -cd 'A-Za-z0-9-')
|
||||
if systemd-run --quiet --unit="$REVERT_UNIT" --on-active="$REVERT_AFTER" \
|
||||
--timer-property=AccuracySec=1s "$SELF" --revert "$BACKUP"; then
|
||||
log "auto-revert armed: $REVERT_UNIT.timer restores $BACKUP in ${REVERT_AFTER}s unless this run confirms"
|
||||
else
|
||||
log "WARNING: could not arm the auto-revert timer; relying on the in-script checks"
|
||||
REVERT_UNIT=""
|
||||
fi
|
||||
else
|
||||
log "WARNING: auto-revert off (no systemd-run or PXE_NETPLAN_REVERT_AFTER=0)"
|
||||
fi
|
||||
|
||||
# An SSH drop (HUP) must not stop us half-way; TERM/INT restore.
|
||||
trap '' HUP
|
||||
trap 'fail "interrupted"' INT TERM
|
||||
|
||||
# 5. install, moving aside the other wired configs (an en* DHCP match would
|
||||
# put a DHCP client on the imaging NIC too)
|
||||
while IFS= read -r f; do
|
||||
[ -n "$f" ] || continue
|
||||
log "moving aside $f (copy in $BACKUP)"
|
||||
rm -f "$f"
|
||||
done < <(other_files)
|
||||
install -m 600 "$STAGED" "$TARGET"
|
||||
netplan generate || fail "netplan generate failed"
|
||||
|
||||
# 6. stable names now, so dnsmasq/UFW/Samba (which use them) work before
|
||||
# the next reboot. Does not depend on the netplan version renaming.
|
||||
[ ${#FREE[@]} -gt 0 ] && log "freeing names for the catch-all: $EXTRA"
|
||||
set_names "$PXE_MAC" "$PXE_NAME" "$CORP_MAC" "$CORP_NAME" "${FREE[@]}" || fail "NIC rename failed"
|
||||
|
||||
# 7. apply
|
||||
netplan apply || fail "netplan apply failed"
|
||||
|
||||
# 8. check
|
||||
names_ok || fail "$PXE_NAME${CORP_NAME:+ / $CORP_NAME} not on MAC $PXE_MAC${CORP_MAC:+ / $CORP_MAC} after apply"
|
||||
ok=0
|
||||
for _ in $(seq 1 "$PXE_WAIT"); do
|
||||
if has_v4 "$PXE_NAME"; then ok=1; break; fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$ok" -eq 1 ] || fail "$PXE_NAME has no IPv4 address after ${PXE_WAIT}s"
|
||||
for m in $OTHERS_V4; do
|
||||
ok=0
|
||||
for _ in $(seq 1 "$CORP_WAIT"); do
|
||||
n=$(iface_of "$m")
|
||||
if [ -n "$n" ] && has_v4 "$n"; then ok=1; break; fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$ok" -eq 1 ] || fail "the NIC with MAC $m had a DHCP IPv4 before and has none after ${CORP_WAIT}s"
|
||||
done
|
||||
if [ -z "$CORP_MAC" ]; then
|
||||
:
|
||||
elif [ "$CORP_HAD_V4" -eq 1 ]; then
|
||||
ok=0
|
||||
for _ in $(seq 1 "$CORP_WAIT"); do
|
||||
if has_v4 "$CORP_NAME"; then ok=1; break; fi
|
||||
sleep 1
|
||||
done
|
||||
[ "$ok" -eq 1 ] || fail "$CORP_NAME had IPv4 before and has none after ${CORP_WAIT}s"
|
||||
elif ! has_v4 "$CORP_NAME"; then
|
||||
log "WARNING: $CORP_NAME has no IPv4 address (it had none before either: corp cable unplugged or no DHCP)"
|
||||
fi
|
||||
while IFS= read -r peer; do
|
||||
[ -n "$peer" ] || continue
|
||||
dev=$(route_dev "$peer")
|
||||
if [ "$dev" = "$PXE_NAME" ] && ! in_subnet "$peer" "${PXE_SUBNET:-}"; then
|
||||
fail "the SSH client $peer now routes through $PXE_NAME"
|
||||
fi
|
||||
if [ -z "$dev" ]; then
|
||||
fail "no route to the SSH client $peer after apply"
|
||||
fi
|
||||
done < <(ssh_peers)
|
||||
|
||||
touch "$BACKUP.committed"
|
||||
cancel_revert
|
||||
trap - INT TERM
|
||||
ip -br addr show dev "$PXE_NAME" 2>/dev/null
|
||||
[ -n "$CORP_NAME" ] && ip -br addr show dev "$CORP_NAME" 2>/dev/null
|
||||
echo CHANGED
|
||||
exit 0
|
||||
145
playbook/pxe-server-helpers/pxe-nic-carrier
Executable file
145
playbook/pxe-server-helpers/pxe-nic-carrier
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# pxe-nic-carrier - re-check the imaging NIC whenever its cable moves.
|
||||
#
|
||||
# The dnsmasq guard (pxe-nic-check --guard) runs only when dnsmasq starts,
|
||||
# and the watch timer only every 5 min. If the imaging cable is moved into
|
||||
# a GE port while dnsmasq runs, dnsmasq would answer DHCP on the GE LAN
|
||||
# until the next watch. This hook closes that gap:
|
||||
#
|
||||
# carrier lost dnsmasq is stopped at once (if it was running) and a
|
||||
# marker remembers that this hook stopped it.
|
||||
# carrier back pxe-nic-carrier.service runs "pxe-nic-carrier --gain":
|
||||
# after a 2 s settle, if dnsmasq is running or the marker
|
||||
# is set, "systemctl restart dnsmasq" - its ExecStartPre
|
||||
# guard checks the NIC while dnsmasq is down and refuses
|
||||
# to start it on the GE network. Otherwise it only runs
|
||||
# the guard to refresh /run/pxe-nic-status.json. dnsmasq
|
||||
# that an operator or the watch stopped stays stopped.
|
||||
#
|
||||
# Only the imaging NIC (and, for a bridge, its ports) counts, as resolved
|
||||
# by "pxe-nic-check --print-imaging" from /etc/pxe-server/nic.conf or the
|
||||
# dnsmasq interface= pin. Events on any other NIC (the GE NIC) exit at
|
||||
# once and touch nothing. Link state is read from sysfs, not from the
|
||||
# event name, and remembered per interface, so the same transition seen
|
||||
# twice (carrier, then routable) acts once.
|
||||
#
|
||||
# Called by networkd-dispatcher with IFACE and STATE in the environment
|
||||
# (installed as /etc/networkd-dispatcher/<state>.d/50-pxe-nic-carrier for
|
||||
# carrier, degraded, degraded-carrier, enslaved, routable, no-carrier and
|
||||
# off), or by hand: pxe-nic-carrier IFACE. Logs: journalctl -t pxe-nic-carrier
|
||||
#
|
||||
# The dispatcher waits for this script, so the event path never runs a
|
||||
# check itself: loss is two systemctl calls, gain hands off to the service.
|
||||
|
||||
set -u
|
||||
|
||||
CHECK="${PXE_NIC_CHECK:-/usr/local/sbin/pxe-nic-check}"
|
||||
SYS_NET="${PXE_SYS_NET:-/sys/class/net}"
|
||||
RUN="${PXE_NIC_CARRIER_RUN:-/run/pxe-nic-carrier}"
|
||||
SYSTEMCTL="${PXE_SYSTEMCTL:-systemctl}"
|
||||
SETTLE="${PXE_NIC_CARRIER_SETTLE:-2}"
|
||||
GAIN_UNIT="pxe-nic-carrier.service"
|
||||
TAG="pxe-nic-carrier"
|
||||
|
||||
PATH="${PATH:+$PATH:}/usr/local/sbin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
|
||||
log() { # log PRIORITY MESSAGE
|
||||
logger -t "$TAG" -p "daemon.$1" -- "$2" 2>/dev/null || \
|
||||
echo "$TAG: $2" >&2
|
||||
}
|
||||
|
||||
imaging_ifaces() {
|
||||
"$CHECK" --print-imaging 2>/dev/null
|
||||
}
|
||||
|
||||
carrier_of() { # 1 if the interface has carrier, else 0 (down reads EINVAL)
|
||||
local c
|
||||
c=$(cat "$SYS_NET/$1/carrier" 2>/dev/null) || c=0
|
||||
[ "$c" = 1 ] && echo 1 || echo 0
|
||||
}
|
||||
|
||||
any_carrier() { # any imaging interface or port with carrier
|
||||
local i
|
||||
for i in "$@"; do
|
||||
[ "$(carrier_of "$i")" = 1 ] && return 0
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
dnsmasq_running() { # active, starting or reloading
|
||||
case "$("$SYSTEMCTL" is-active dnsmasq 2>/dev/null)" in
|
||||
active|activating|reloading) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
on_event() {
|
||||
local iface="$1" list=() match="" i now prev
|
||||
mapfile -t list < <(imaging_ifaces)
|
||||
for i in "${list[@]}"; do
|
||||
[ -n "$i" ] && [ "$i" = "$iface" ] && match=1
|
||||
done
|
||||
[ -n "$match" ] || exit 0 # not the imaging NIC: hands off
|
||||
|
||||
mkdir -p "$RUN" || exit 0
|
||||
exec 9>"$RUN/event.lock"
|
||||
flock -w 10 9 || log warning "lock busy, handling $iface anyway"
|
||||
|
||||
now=$(carrier_of "$iface")
|
||||
prev=$(cat "$RUN/link.$iface" 2>/dev/null) || prev=unknown
|
||||
echo "$now" > "$RUN/link.$iface"
|
||||
[ "$now" = "$prev" ] && exit 0 # same state again: already handled
|
||||
|
||||
if [ "$now" = 0 ]; then
|
||||
# an in-flight gain check must not restart dnsmasq after this
|
||||
"$SYSTEMCTL" stop "$GAIN_UNIT" >/dev/null 2>&1
|
||||
if dnsmasq_running; then
|
||||
: > "$RUN/stopped-dnsmasq"
|
||||
"$SYSTEMCTL" stop --no-block dnsmasq
|
||||
log warning "carrier lost on imaging NIC $iface (${STATE:-event}): stopped dnsmasq; it restarts after the guard passes when the link returns"
|
||||
else
|
||||
log info "carrier lost on imaging NIC $iface (${STATE:-event}); dnsmasq not running"
|
||||
fi
|
||||
else
|
||||
log notice "carrier up on imaging NIC $iface (${STATE:-event}, was $prev): re-checking before dnsmasq serves"
|
||||
"$SYSTEMCTL" restart --no-block "$GAIN_UNIT"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
on_gain() {
|
||||
local list=() rc
|
||||
sleep "$SETTLE"
|
||||
mapfile -t list < <(imaging_ifaces)
|
||||
if [ "${#list[@]}" -eq 0 ] || [ -z "${list[0]}" ]; then
|
||||
log err "imaging NIC unknown, nothing to re-check"
|
||||
exit 0
|
||||
fi
|
||||
if ! any_carrier "${list[@]}"; then
|
||||
log info "imaging NIC ${list[0]} lost carrier again, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
if dnsmasq_running || [ -e "$RUN/stopped-dnsmasq" ]; then
|
||||
# restart = stop, then the ExecStartPre guard with dnsmasq down
|
||||
"$SYSTEMCTL" restart dnsmasq
|
||||
rc=$?
|
||||
rm -f "$RUN/stopped-dnsmasq"
|
||||
if [ "$rc" -eq 0 ]; then
|
||||
log notice "imaging NIC ${list[0]}: guard passed, dnsmasq (re)started"
|
||||
else
|
||||
log crit "imaging NIC ${list[0]}: dnsmasq NOT started (guard refused or start failed, rc $rc); see journalctl -u dnsmasq. Fix the cabling, then systemctl start dnsmasq"
|
||||
fi
|
||||
else
|
||||
"$CHECK" --guard >/dev/null 2>&1
|
||||
rc=$?
|
||||
log info "imaging NIC ${list[0]}: dnsmasq was not running, left stopped (guard rc $rc, status refreshed)"
|
||||
fi
|
||||
exit 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--gain) on_gain ;;
|
||||
-h|--help) sed -n '2,36p' "$0"; exit 0 ;;
|
||||
*) on_event "${1:-${IFACE:-}}" ;;
|
||||
esac
|
||||
16
playbook/pxe-server-helpers/pxe-nic-carrier.service
Normal file
16
playbook/pxe-server-helpers/pxe-nic-carrier.service
Normal file
@@ -0,0 +1,16 @@
|
||||
# Started (never enabled) by pxe-nic-carrier, the networkd-dispatcher
|
||||
# hook, when the imaging NIC gets carrier back. Re-checks the imaging NIC
|
||||
# before dnsmasq serves again: if dnsmasq was running (or the hook stopped
|
||||
# it on carrier loss), "systemctl restart dnsmasq" so its ExecStartPre
|
||||
# guard decides; otherwise it only refreshes /run/pxe-nic-status.json.
|
||||
# The hook restarts this unit on every carrier gain and stops it on a
|
||||
# carrier loss, so the newest event wins.
|
||||
|
||||
[Unit]
|
||||
Description=PXE NIC carrier change: re-check the imaging NIC before dnsmasq serves
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/pxe-nic-carrier --gain
|
||||
TimeoutStartSec=120
|
||||
1422
playbook/pxe-server-helpers/pxe-nic-check
Executable file
1422
playbook/pxe-server-helpers/pxe-nic-check
Executable file
File diff suppressed because it is too large
Load Diff
17
playbook/pxe-server-helpers/pxe-nic-check.service
Normal file
17
playbook/pxe-server-helpers/pxe-nic-check.service
Normal file
@@ -0,0 +1,17 @@
|
||||
# Periodic imaging-NIC watch (pxe-nic-check.timer runs this every 5 min).
|
||||
# Listens 60 s on each NIC, probes the imaging NIC for foreign DHCP, writes
|
||||
# /run/pxe-nic-status.json for the webapp, and STOPS dnsmasq if the imaging
|
||||
# NIC turns out to be on the GE network or next to another DHCP server.
|
||||
# It never starts dnsmasq: after fixing the cabling, start it by hand
|
||||
# (systemctl start dnsmasq), which re-runs the guard. If dnsmasq was
|
||||
# restarted inside the window it re-checks (~6 s) before stopping it.
|
||||
|
||||
[Unit]
|
||||
Description=PXE NIC watch: keep dnsmasq off the GE corporate network
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/local/sbin/pxe-nic-check --watch
|
||||
TimeoutStartSec=180
|
||||
Nice=5
|
||||
11
playbook/pxe-server-helpers/pxe-nic-check.timer
Normal file
11
playbook/pxe-server-helpers/pxe-nic-check.timer
Normal file
@@ -0,0 +1,11 @@
|
||||
[Unit]
|
||||
Description=PXE NIC watch every 5 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnActiveSec=1min
|
||||
OnUnitActiveSec=5min
|
||||
AccuracySec=15s
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
297
playbook/pxe-server-helpers/pxe-pick-nic
Executable file
297
playbook/pxe-server-helpers/pxe-pick-nic
Executable file
@@ -0,0 +1,297 @@
|
||||
#!/bin/bash
|
||||
# pxe-pick-nic - choose the imaging NIC by its cable, not by its MAC.
|
||||
#
|
||||
# Usage:
|
||||
# sudo pxe-pick-nic interactive: pull the imaging switch cable and
|
||||
# plug it back in; the port that blinked is it
|
||||
# pxe-pick-nic --auto for the first-boot playbook: the one wired
|
||||
# port with a link, if it is isolated
|
||||
# pxe-pick-nic --show print the saved choice
|
||||
#
|
||||
# The choice is saved in /etc/pxe-server/nic-pick.conf (imaging_mac=...).
|
||||
# The playbook uses it when site-vars.yml leaves pxe_nic_mac empty, and pins
|
||||
# that MAC from then on: the box never re-guesses at boot, so a moved cable
|
||||
# can never move 172.16.9.1 and DHCP onto another port.
|
||||
#
|
||||
# A port is accepted only when pxe-nic-check calls it "isolated": no GE
|
||||
# switch (LLDP/CDP), no 802.1X port authentication, and no other DHCP
|
||||
# server answering one DHCPDISCOVER. It listens CHECK_WINDOW seconds
|
||||
# (default 65: a Cisco switch sends CDP every 60 s). --auto also refuses a
|
||||
# port where more than ARP_MAX (default 10) hosts were heard: an imaging
|
||||
# switch at install time is nearly silent, a GE access VLAN is not, and
|
||||
# that holds even when the switch sends no LLDP/CDP and the probe is lost
|
||||
# (STP still listening). Anything else is refused and nothing is saved.
|
||||
# The check uses the site's corp_domain_suffixes and allowed_switches
|
||||
# (PXE_PICK_SUFFIXES / PXE_PICK_ALLOWED from the playbook, else
|
||||
# /etc/pxe-server/nic.conf), so a managed imaging switch can be allowed.
|
||||
#
|
||||
# Install rule for sites: during the install plug in ONLY the imaging
|
||||
# switch. Plug the GE cable into any other port afterwards; the catch-all
|
||||
# netplan entry makes it a DHCP client and dnsmasq never serves it.
|
||||
#
|
||||
# Exit: 0 chosen (or --show found one), 1 refused / could not choose,
|
||||
# 2 usage.
|
||||
|
||||
set -u
|
||||
|
||||
CONF=${PXE_PICK_CONF:-/etc/pxe-server/nic-pick.conf}
|
||||
HINT=${PXE_PICK_HINT:-/var/lib/pxe-firstboot/nic-hint}
|
||||
SYSFS_NET=${SYSFS_NET:-/sys/class/net}
|
||||
NIC_CHECK=${PXE_NIC_CHECK:-/usr/local/sbin/pxe-nic-check}
|
||||
CHECK_WINDOW=${CHECK_WINDOW:-65}
|
||||
ARP_MAX=${PXE_PICK_ARP_MAX:-10}
|
||||
NIC_CONF=${PXE_NIC_CONF:-/etc/pxe-server/nic.conf}
|
||||
SETTLE=${PXE_PICK_SETTLE:-8}
|
||||
WAIT=${PXE_PICK_WAIT:-180}
|
||||
|
||||
log() { echo "pxe-pick-nic: $*"; }
|
||||
|
||||
# Physical wired Ethernet ports: a device behind them, ARP type 1, not
|
||||
# wireless, not a bridge.
|
||||
wired() {
|
||||
local d n
|
||||
for d in "$SYSFS_NET"/*; do
|
||||
n=${d##*/}
|
||||
[ -e "$d/device" ] || continue
|
||||
[ "$(cat "$d/type" 2>/dev/null)" = 1 ] || continue
|
||||
[ -e "$d/wireless" ] || [ -e "$d/phy80211" ] && continue
|
||||
[ -e "$d/bridge" ] && continue
|
||||
echo "$n"
|
||||
done
|
||||
}
|
||||
|
||||
mac_of() { cat "$SYSFS_NET/$1/address" 2>/dev/null; }
|
||||
|
||||
# 1 = link, 0 = none. carrier is unreadable while the port is admin-down.
|
||||
carrier() { [ "$(cat "$SYSFS_NET/$1/carrier" 2>/dev/null)" = 1 ] && echo 1 || echo 0; }
|
||||
|
||||
# Bring every wired port up (no address is added) so its link can be seen.
|
||||
links_up() {
|
||||
local n up=0
|
||||
for n in $(wired); do
|
||||
if ! ip link show dev "$n" 2>/dev/null | grep -q '[<,]UP[,>]'; then
|
||||
ip link set dev "$n" up 2>/dev/null && up=1
|
||||
fi
|
||||
done
|
||||
[ "$up" -eq 1 ] && sleep "$SETTLE"
|
||||
return 0
|
||||
}
|
||||
|
||||
table() {
|
||||
local n
|
||||
for n in $(wired); do
|
||||
printf ' %-16s %s link %s\n' "$n" "$(mac_of "$n")" \
|
||||
"$([ "$(carrier "$n")" = 1 ] && echo up || echo down)"
|
||||
done
|
||||
}
|
||||
|
||||
# A pxe-nic-check config with only the site settings that matter here
|
||||
# (never the MAC keys: the port under test is given with --iface).
|
||||
check_config() {
|
||||
local f=$1 suffixes=${PXE_PICK_SUFFIXES:-} allowed=${PXE_PICK_ALLOWED:-}
|
||||
if [ -z "$suffixes$allowed" ] && [ -f "$NIC_CONF" ]; then
|
||||
suffixes=$(sed -n 's/^[[:space:]]*corp_domain_suffixes[[:space:]]*=[[:space:]]*//p' "$NIC_CONF" | tail -1)
|
||||
allowed=$(sed -n 's/^[[:space:]]*allowed_switches[[:space:]]*=[[:space:]]*//p' "$NIC_CONF" | tail -1)
|
||||
fi
|
||||
{
|
||||
echo "[nic]"
|
||||
[ -n "$suffixes" ] && echo "corp_domain_suffixes = $suffixes"
|
||||
[ -n "$allowed" ] && echo "allowed_switches = $allowed"
|
||||
} >"$f"
|
||||
}
|
||||
|
||||
# check IFACE [strict]: prints the verdict, returns 0 only for "isolated"
|
||||
# (with strict, also only when at most ARP_MAX hosts were heard).
|
||||
check() {
|
||||
local out verdict arp why cfg
|
||||
if [ ! -f "$NIC_CHECK" ]; then
|
||||
log "ERROR: $NIC_CHECK not found"
|
||||
return 1
|
||||
fi
|
||||
cfg=$(mktemp)
|
||||
check_config "$cfg"
|
||||
log "checking $1 is isolated: listening ${CHECK_WINDOW}s for GE switches and DHCP servers..."
|
||||
out=$(python3 "$NIC_CHECK" --iface "$1" --probe --window "$CHECK_WINDOW" --json --config "$cfg" 2>/dev/null)
|
||||
rm -f "$cfg"
|
||||
read -r verdict arp why < <(python3 -c '
|
||||
import json, sys
|
||||
try:
|
||||
e = json.load(sys.stdin)["interfaces"][0]
|
||||
ev = "; ".join(x for x in e["evidence"] if not x.endswith("ARP hosts seen"))
|
||||
print(e["verdict"], int(e.get("arp_hosts") or 0), ev or "-")
|
||||
except Exception as ex:
|
||||
print("error", 0, "pxe-nic-check gave no usable result: %s" % ex)
|
||||
' <<<"$out")
|
||||
if [ "$verdict" = isolated ]; then
|
||||
if [ "$arp" -gt "$ARP_MAX" ]; then
|
||||
log "$1: isolated by its switch and DHCP, but $arp hosts were heard (more than $ARP_MAX)"
|
||||
if [ "${2:-}" = strict ]; then
|
||||
log "REFUSED: a busy LAN is not an imaging switch at install time; it may be a GE port"
|
||||
log "with LLDP/CDP off. Pick it by its cable (sudo pxe-pick-nic) or set pxe_nic_mac."
|
||||
return 1
|
||||
fi
|
||||
log "WARNING: make sure this is the imaging switch, not a GE port with LLDP/CDP turned off."
|
||||
else
|
||||
log "$1: isolated (no GE switch, no 802.1X, no other DHCP server, $arp hosts heard)"
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
log "$1: verdict $verdict: $why"
|
||||
case "$verdict" in
|
||||
ge) log "That port is on the GE network (or a managed switch not in allowed_switches). It must never be the imaging port." ;;
|
||||
foreign-dhcp) log "Another DHCP server answers on that port (a second PXE server, or a routed LAN)." ;;
|
||||
no-link) log "That port lost its link during the check." ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
save() {
|
||||
local n=$1 mac
|
||||
mac=$(mac_of "$n")
|
||||
mkdir -p "$(dirname "$CONF")"
|
||||
{
|
||||
echo "# Written by pxe-pick-nic $(date -Is). The imaging NIC; the playbook"
|
||||
echo "# pins it when site-vars.yml leaves pxe_nic_mac empty."
|
||||
echo "imaging_mac=$mac"
|
||||
echo "picked_iface=$n"
|
||||
} >"$CONF.tmp" && mv "$CONF.tmp" "$CONF"
|
||||
rm -f "$HINT"
|
||||
log "saved: imaging NIC = $n ($mac) in $CONF"
|
||||
}
|
||||
|
||||
hint() {
|
||||
mkdir -p "$(dirname "$HINT")" 2>/dev/null
|
||||
{ printf '%s\n' "$*" >"$HINT"; } 2>/dev/null
|
||||
}
|
||||
|
||||
saved_mac() {
|
||||
sed -n 's/^imaging_mac=\(\([0-9a-f]\{2\}:\)\{5\}[0-9a-f]\{2\}\)$/\1/p' "$CONF" 2>/dev/null | tail -1
|
||||
}
|
||||
|
||||
# --- modes ---------------------------------------------------------------------
|
||||
|
||||
mode_show() {
|
||||
local mac
|
||||
mac=$(saved_mac)
|
||||
[ -n "$mac" ] || { log "no saved choice in $CONF"; return 1; }
|
||||
echo "$mac"
|
||||
}
|
||||
|
||||
mode_auto() {
|
||||
local mac n with=() names
|
||||
mac=$(saved_mac)
|
||||
if [ -n "$mac" ]; then
|
||||
for n in $(wired); do
|
||||
if [ "$(mac_of "$n")" = "$mac" ]; then
|
||||
log "using the saved choice: $n ($mac)"
|
||||
echo "$mac"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
log "the saved imaging NIC $mac is not on this box any more"
|
||||
hint "The saved imaging NIC $mac is gone (dongle replaced?). Log in and run: sudo pxe-pick-nic"
|
||||
return 1
|
||||
fi
|
||||
links_up
|
||||
for n in $(wired); do
|
||||
[ "$(carrier "$n")" = 1 ] && with+=("$n")
|
||||
done
|
||||
log "wired ports:"
|
||||
table
|
||||
if [ ${#with[@]} -eq 0 ]; then
|
||||
log "REFUSED: no wired port has a link. Plug the imaging switch into the port to use."
|
||||
hint "Could not pick the imaging port: no port has a link. Plug in the imaging switch, then log in and run: sudo pxe-pick-nic"
|
||||
return 1
|
||||
fi
|
||||
if [ ${#with[@]} -gt 1 ]; then
|
||||
names="${with[*]}"
|
||||
log "REFUSED: ${#with[@]} ports have a link ($names). During the install only the imaging switch may be plugged in."
|
||||
hint "Could not pick the imaging port: ${#with[@]} ports have a link ($names). Log in and run: sudo pxe-pick-nic"
|
||||
return 1
|
||||
fi
|
||||
n=${with[0]}
|
||||
if ! check "$n" strict; then
|
||||
hint "Could not pick the imaging port: $n is not isolated (see /var/log/first-boot.log). Plug only the imaging switch in, then log in and run: sudo pxe-pick-nic"
|
||||
return 1
|
||||
fi
|
||||
save "$n" >&2
|
||||
mac_of "$n"
|
||||
}
|
||||
|
||||
# Wait until exactly one port changes its link; print "NAME NEWSTATE".
|
||||
wait_change() {
|
||||
local -A before=()
|
||||
local n t=0 changed=()
|
||||
for n in $(wired); do before[$n]=$(carrier "$n"); done
|
||||
while [ "$t" -lt $((WAIT * 2)) ]; do
|
||||
sleep 0.5
|
||||
t=$((t + 1))
|
||||
changed=()
|
||||
for n in "${!before[@]}"; do
|
||||
[ "$(carrier "$n")" != "${before[$n]}" ] && changed+=("$n")
|
||||
done
|
||||
if [ ${#changed[@]} -eq 1 ]; then
|
||||
sleep 1 # a flapping connector settles
|
||||
n=${changed[0]}
|
||||
echo "$n $(carrier "$n")"
|
||||
return 0
|
||||
fi
|
||||
if [ ${#changed[@]} -gt 1 ]; then
|
||||
log "more than one port changed (${changed[*]}); move one cable at a time" >&2
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
log "nothing changed in ${WAIT}s" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
mode_interactive() {
|
||||
local n state again ans
|
||||
[ -t 0 ] || { log "interactive mode needs a terminal (use --auto from scripts)"; return 2; }
|
||||
[ "$(id -u)" -eq 0 ] || { log "run it as root: sudo pxe-pick-nic"; return 2; }
|
||||
links_up
|
||||
echo
|
||||
echo "Pick the imaging NIC: the port cabled to the isolated imaging switch."
|
||||
echo "Wired ports now:"
|
||||
table
|
||||
echo
|
||||
echo "Pull the imaging switch cable out of this server now"
|
||||
echo "(or, if it is not plugged in yet, plug it in now). Waiting up to ${WAIT}s..."
|
||||
read -r n state < <(wait_change) || return 1
|
||||
[ -n "${n:-}" ] || return 1
|
||||
if [ "$state" = 0 ]; then
|
||||
echo "$n lost its link. Plug the cable back into the SAME port..."
|
||||
read -r again state < <(wait_change) || return 1
|
||||
if [ "${again:-}" != "$n" ] || [ "$state" != 1 ]; then
|
||||
log "expected $n to come back up, saw ${again:-nothing} $state; try again"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
sleep "$SETTLE"
|
||||
echo "Imaging port: $n ($(mac_of "$n"))"
|
||||
check "$n" || { log "nothing saved"; return 1; }
|
||||
read -rp "Use $n ($(mac_of "$n")) as the imaging NIC? [y/N] " ans
|
||||
[[ "$ans" =~ ^[Yy]$ ]] || { log "nothing saved"; return 1; }
|
||||
save "$n"
|
||||
if [ -e /var/lib/pxe-firstboot.failed ] && [ -x /opt/first-boot.sh ]; then
|
||||
read -rp "Run the server setup again now? [Y/n] " ans
|
||||
if [[ ! "$ans" =~ ^[Nn]$ ]]; then
|
||||
rm -f /var/lib/pxe-firstboot/attempts
|
||||
/opt/first-boot.sh 2>&1 | tee -a /var/log/first-boot.log
|
||||
return "${PIPESTATUS[0]}"
|
||||
fi
|
||||
echo "Run it later with: sudo rm -f /var/lib/pxe-firstboot/attempts; sudo /opt/first-boot.sh"
|
||||
else
|
||||
echo "Saved. If the server is already set up, re-run the playbook from the"
|
||||
echo "installer USB to move the imaging config onto this port."
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
--auto) mode_auto ;;
|
||||
--show) mode_show ;;
|
||||
"") mode_interactive ;;
|
||||
-h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
|
||||
*) echo "usage: $0 [--auto | --show]" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -15,7 +15,7 @@
|
||||
# Step 3: restart smbd (full smbd restart, kills all child sessions)
|
||||
# Step 4: kill any leftover smbd child processes that survived restart
|
||||
# Step 5: flush conntrack for 172.16.9.0/24 (kernel connection tracking)
|
||||
# Step 6: flush ARP / neighbour cache on br-pxe
|
||||
# Step 6: flush ARP / neighbour cache
|
||||
# Step 7: drop TCP sockets on port 445 via ss -K
|
||||
# Step 8: restart dnsmasq (DHCP/TFTP state as a last resort before reboot)
|
||||
|
||||
@@ -65,8 +65,8 @@ else
|
||||
fi
|
||||
pause "Step 5 done"
|
||||
|
||||
echo "=== Step 6/8: flush ARP / neighbour cache on br-pxe ==="
|
||||
ip neigh flush dev br-pxe 2>&1 || ip neigh flush all 2>&1 || true
|
||||
echo "=== Step 6/8: flush ARP / neighbour cache ==="
|
||||
ip neigh flush all 2>&1 || true
|
||||
pause "Step 6 done"
|
||||
|
||||
echo "=== Step 7/8: drop tcp sockets on port 445 ==="
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
170
playbook/scripts/preflight.ps1
Executable file
170
playbook/scripts/preflight.ps1
Executable file
@@ -0,0 +1,170 @@
|
||||
# preflight.ps1 - check the things PESetup fails on, before it fails on them.
|
||||
#
|
||||
# Called by startnet.cmd once the media is mapped. Prints a short report a tech
|
||||
# can read at the bay and exits non-zero if a check is fatal.
|
||||
#
|
||||
# WHY EACH CHECK IS HERE - all four come from PESetup's own behaviour
|
||||
# (docs/PESETUP-INTERNALS.md, decompiled 4.0.0.17, observed on 4.0.0.20):
|
||||
#
|
||||
# Secure boot GatherDataSelection fails the step outright when
|
||||
# SecurebootEnabled != 1. Hard failure, minutes into a build.
|
||||
# Disk size MinRequiredSpaceWithoutCompression is 128849018880 (120 GB).
|
||||
# Driver match GetDriverByModel returns null on a miss and PESetup logs a
|
||||
# WARNING and keeps going. The bay images with no drivers, so no
|
||||
# NIC, so DNS fails at first boot and enrollment cannot reach the
|
||||
# CDN. The symptom appears far from the cause - this is the check
|
||||
# that earns the script.
|
||||
# Media age The media expires 30 days after build. PESetup shows days-left
|
||||
# on a screen nobody reads.
|
||||
#
|
||||
# Lives on the enrollment share so it can be fixed without rebuilding boot.wim.
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$MediaDrive = 'Z:',
|
||||
[int]$MinDiskGB = 120,
|
||||
[int]$MediaWarnDays = 25
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$fatal = 0
|
||||
$warn = 0
|
||||
|
||||
function Ok { param($m) Write-Host (" [ OK ] " + $m) }
|
||||
function Warn { param($m) Write-Host (" [WARN] " + $m); $script:warn++ }
|
||||
function Fail { param($m) Write-Host (" [FAIL] " + $m); $script:fatal++ }
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "======== Pre-imaging checks ========"
|
||||
|
||||
# --- 1. Secure boot ------------------------------------------------------
|
||||
# Confirm-SecureBootUEFI is not always present in WinPE; read the state the
|
||||
# firmware exposes instead.
|
||||
try {
|
||||
$sb = Get-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State' `
|
||||
-Name UEFISecureBootEnabled -ErrorAction Stop
|
||||
if ($sb.UEFISecureBootEnabled -eq 1) {
|
||||
Ok "Secure boot enabled"
|
||||
} else {
|
||||
Fail "Secure boot is OFF. PESetup will fail at GatherData. Enable it in BIOS."
|
||||
}
|
||||
} catch {
|
||||
Warn "Could not read secure boot state - if this is a legacy/CSM boot, PESetup will fail."
|
||||
}
|
||||
|
||||
# --- 2. Disk size --------------------------------------------------------
|
||||
try {
|
||||
$disk = Get-CimInstance Win32_DiskDrive -ErrorAction Stop |
|
||||
Where-Object { $_.MediaType -like '*Fixed*' } |
|
||||
Sort-Object Index | Select-Object -First 1
|
||||
if ($disk) {
|
||||
$gb = [math]::Round($disk.Size / 1GB, 1)
|
||||
if ($gb -ge $MinDiskGB) {
|
||||
Ok ("Disk 0 is {0} GB ({1})" -f $gb, $disk.Model)
|
||||
} else {
|
||||
Fail ("Disk 0 is only {0} GB; PESetup needs {1} GB. ({2})" -f $gb, $MinDiskGB, $disk.Model)
|
||||
}
|
||||
} else {
|
||||
Fail "No fixed disk found. PESetup has nothing to image."
|
||||
}
|
||||
} catch {
|
||||
Warn "Could not enumerate disks: $_"
|
||||
}
|
||||
|
||||
# --- 3. Driver match for THIS model --------------------------------------
|
||||
# Reimplements GetDriverByModel: family filter first (it knows only Latitude,
|
||||
# OptiPlex and Precision), then a substring test of comma-separated tokens,
|
||||
# first match wins. Tokens are NOT trimmed, matching the C#.
|
||||
try {
|
||||
$model = (Get-CimInstance Win32_ComputerSystem -ErrorAction Stop).Model
|
||||
$catalogue = Join-Path $MediaDrive 'Deploy\Control\HardwareDriver.json'
|
||||
if (-not (Test-Path $catalogue)) {
|
||||
Warn "HardwareDriver.json not found at $catalogue - cannot check drivers."
|
||||
} else {
|
||||
# NOT ConvertFrom-Json. HardwareDriver.json carries both casings of the
|
||||
# same fields - "fileName" and "FileName", "destinationDir" and
|
||||
# "DestinationDir". Windows PowerShell 5.1 (which is what WinPE runs)
|
||||
# treats object keys case-insensitively and throws:
|
||||
# "cannot convert the JSON string because a dictionary that was
|
||||
# converted from the string contains the duplicated keys 'FileName'
|
||||
# and 'FileName'"
|
||||
# -AsHashtable would handle it but that is PowerShell 6+. So pull the
|
||||
# four fields we need out of each entry by text instead. Prefers the
|
||||
# lowercase key, falls back to the capitalised one.
|
||||
$raw = Get-Content $catalogue -Raw
|
||||
$entries = @()
|
||||
foreach ($chunk in ([regex]::Split($raw, '\}\s*,\s*\{'))) {
|
||||
$get = {
|
||||
param($names)
|
||||
foreach ($n in $names) {
|
||||
$m = [regex]::Match($chunk, '"' + $n + '"\s*:\s*"((?:[^"\\]|\\.)*)"')
|
||||
if ($m.Success) { return $m.Groups[1].Value -replace '\\\\', '\' }
|
||||
}
|
||||
return ''
|
||||
}
|
||||
$e = [pscustomobject]@{
|
||||
modelswminame = (& $get @('modelswminame','models'))
|
||||
family = (& $get @('family'))
|
||||
fileName = (& $get @('fileName','FileName'))
|
||||
destinationDir = (& $get @('destinationDir','DestinationDir'))
|
||||
}
|
||||
if ($e.modelswminame) { $entries += $e }
|
||||
}
|
||||
if (-not $entries.Count) {
|
||||
Warn "Could not extract any entries from $catalogue - driver check skipped."
|
||||
}
|
||||
$family = ''
|
||||
if ($model.ToUpper().Contains('LATITUDE')) { $family = 'Latitude' }
|
||||
if ($model.ToUpper().Contains('OPTIPLEX')) { $family = 'Optiplex' }
|
||||
if ($model.ToUpper().Contains('PRECISION')) { $family = 'Precision' }
|
||||
|
||||
$hit = $null
|
||||
foreach ($e in $entries) {
|
||||
if ($family -and ($e.family -notlike "*$family*")) { continue }
|
||||
foreach ($tok in ([string]$e.modelswminame).Split(',')) {
|
||||
if ($tok -and $model.ToLower().Contains($tok.ToLower())) { $hit = $e; break }
|
||||
}
|
||||
if ($hit) { break }
|
||||
}
|
||||
if ($hit) {
|
||||
$zip = Join-Path $MediaDrive (([string]$hit.destinationDir) -replace '\*destinationdir\*\\?','')
|
||||
$zip = Join-Path $zip $hit.fileName
|
||||
if (Test-Path $zip) {
|
||||
Ok ("Driver pack for '{0}': {1}" -f $model, $hit.fileName)
|
||||
} else {
|
||||
Fail ("Driver pack for '{0}' is listed but MISSING on the media: {1}" -f $model, $hit.fileName)
|
||||
}
|
||||
} else {
|
||||
Fail ("NO driver pack matches '{0}'. PESetup logs this as a warning only - the bay will image with NO network drivers." -f $model)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Warn "Driver check failed: $_"
|
||||
}
|
||||
|
||||
# --- 4. Media age --------------------------------------------------------
|
||||
# Approximate: PESetup expires media 30 days after build and Media.tag is
|
||||
# rewritten when the media is rebuilt, so its timestamp is the best local proxy.
|
||||
try {
|
||||
$tag = Join-Path $MediaDrive 'Deploy\Control\Media.tag'
|
||||
if (Test-Path $tag) {
|
||||
$age = [int]((Get-Date) - (Get-Item $tag).LastWriteTime).TotalDays
|
||||
if ($age -ge $MediaWarnDays) {
|
||||
Warn ("Media is about {0} days old; it expires at 30. Rebuild it soon." -f $age)
|
||||
} else {
|
||||
Ok ("Media is about {0} days old" -f $age)
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
|
||||
Write-Host "===================================="
|
||||
if ($fatal -gt 0) {
|
||||
Write-Host ""
|
||||
Write-Host " $fatal BLOCKING problem(s) found. Imaging this bay will not work."
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
if ($warn -gt 0) { Write-Host " $warn warning(s), no blockers." }
|
||||
else { Write-Host " All checks passed." }
|
||||
Write-Host ""
|
||||
exit 0
|
||||
@@ -1,142 +0,0 @@
|
||||
# select-waxtrace-asset.ps1 - Arrow-key bay picker for wax/trace imaging.
|
||||
#
|
||||
# Reads bay-config.csv on the PXE share to build the menu of known bays.
|
||||
# Falls back to INDEX.csv (cal-disc index) if bay-config.csv is missing.
|
||||
# Operator picks with Up/Down arrows + Enter. Always appends an
|
||||
# "Other (new bay)" option at the end for unlisted bays - selecting it
|
||||
# falls back to a free-text prompt.
|
||||
#
|
||||
# Writes the chosen asset tag to $OutFile (one line, no trailing newline).
|
||||
# startnet.cmd reads that file back into the MACHINENUM batch var.
|
||||
#
|
||||
# Runs in WinPE PowerShell. Win10/11 WinPE ships powershell.exe with
|
||||
# System.Console.ReadKey support. Tested 2026-05-18.
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = asset tag written to $OutFile
|
||||
# 1 = user cancelled (Esc) - $OutFile not written
|
||||
# 2 = no readable bay source AND no fallback entered
|
||||
|
||||
param(
|
||||
[string]$IndexPath = 'Y:\installers-post\waxtrace\bay-config.csv',
|
||||
[Parameter(Mandatory=$true)][string]$OutFile
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
function Read-BayList {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return @() }
|
||||
try {
|
||||
$rows = @(Import-Csv -LiteralPath $Path)
|
||||
# bay-config.csv has asset_tag,ftpak_version,model,user_id,hw_sn,hw_id,host,notes
|
||||
# INDEX.csv (legacy) has asset_tag,unit_serial,probe_part,...
|
||||
$isBayCfg = $rows.Count -gt 0 -and ($rows[0].PSObject.Properties.Name -contains 'ftpak_version')
|
||||
return $rows | Sort-Object -Property asset_tag | ForEach-Object {
|
||||
if ($isBayCfg) {
|
||||
[PSCustomObject]@{
|
||||
asset_tag = $_.asset_tag
|
||||
col1 = $_.ftpak_version
|
||||
col2 = $_.model
|
||||
col3 = $_.user_id
|
||||
schema = 'bay-config'
|
||||
}
|
||||
} else {
|
||||
[PSCustomObject]@{
|
||||
asset_tag = $_.asset_tag
|
||||
col1 = $_.unit_serial
|
||||
col2 = $_.probe_part
|
||||
col3 = ''
|
||||
schema = 'index'
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return @()
|
||||
}
|
||||
}
|
||||
|
||||
function Show-Menu {
|
||||
param([object[]]$Items, [int]$Selected, [string]$Title, [string]$Schema)
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-Host " ============================================================"
|
||||
Write-Host " $Title"
|
||||
Write-Host " ============================================================"
|
||||
Write-Host ""
|
||||
Write-Host " Up / Down arrows = navigate, Enter = select, Esc = cancel"
|
||||
Write-Host ""
|
||||
if ($Schema -eq 'bay-config') {
|
||||
Write-Host (" {0,-10} {1,-8} {2,-10} {3}" -f 'ASSET','FTPAK','MODEL','USER ID')
|
||||
Write-Host (" {0,-10} {1,-8} {2,-10} {3}" -f '-----','-----','-----','-------')
|
||||
} else {
|
||||
Write-Host (" {0,-10} {1,-14} {2}" -f 'ASSET','SERIAL','PROBE')
|
||||
Write-Host (" {0,-10} {1,-14} {2}" -f '-----','------','-----')
|
||||
}
|
||||
for ($i = 0; $i -lt $Items.Count; $i++) {
|
||||
$item = $Items[$i]
|
||||
if ($item -is [string]) {
|
||||
$line = $item
|
||||
} elseif ($Schema -eq 'bay-config') {
|
||||
$line = "{0,-10} {1,-8} {2,-10} {3}" -f $item.asset_tag, $item.col1, $item.col2, $item.col3
|
||||
} else {
|
||||
$line = "{0,-10} {1,-14} {2}" -f $item.asset_tag, $item.col1, $item.col2
|
||||
}
|
||||
if ($i -eq $Selected) {
|
||||
Write-Host (" > " + $line) -ForegroundColor Black -BackgroundColor White
|
||||
} else {
|
||||
Write-Host (" " + $line)
|
||||
}
|
||||
}
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# Try bay-config.csv first; fall back to INDEX.csv if missing OR if the
|
||||
# explicit -IndexPath argument points to INDEX.csv (legacy callers).
|
||||
$bays = @(Read-BayList -Path $IndexPath)
|
||||
if ($bays.Count -eq 0 -and $IndexPath -notmatch 'INDEX\.csv$') {
|
||||
$fallback = 'Y:\installers-post\waxtrace\calibrations\INDEX.csv'
|
||||
if (Test-Path -LiteralPath $fallback) {
|
||||
Write-Host " (no bay-config.csv at $IndexPath - falling back to $fallback)"
|
||||
$bays = @(Read-BayList -Path $fallback)
|
||||
}
|
||||
}
|
||||
$schema = if ($bays.Count -gt 0) { $bays[0].schema } else { 'bay-config' }
|
||||
|
||||
$menuItems = @()
|
||||
foreach ($b in $bays) { $menuItems += $b }
|
||||
$menuItems += '** Other (new bay - enter asset tag manually) **'
|
||||
|
||||
$sel = 0
|
||||
while ($true) {
|
||||
Show-Menu -Items $menuItems -Selected $sel -Title 'Wax/Trace Asset Tag' -Schema $schema
|
||||
$key = [System.Console]::ReadKey($true)
|
||||
switch ($key.Key) {
|
||||
'UpArrow' { if ($sel -gt 0) { $sel-- } }
|
||||
'DownArrow' { if ($sel -lt ($menuItems.Count - 1)) { $sel++ } }
|
||||
'Enter' {
|
||||
if ($sel -eq ($menuItems.Count - 1)) {
|
||||
Write-Host ""
|
||||
$manual = Read-Host " Enter asset tag (e.g. WJRP9999) or blank to abort"
|
||||
if ($manual) {
|
||||
$manual = $manual.Trim().ToUpper()
|
||||
Set-Content -LiteralPath $OutFile -Value $manual -NoNewline -Encoding ascii
|
||||
Write-Host ""
|
||||
Write-Host " Saved asset tag: $manual"
|
||||
Start-Sleep -Seconds 1
|
||||
exit 0
|
||||
} else {
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
$pick = $bays[$sel].asset_tag
|
||||
Set-Content -LiteralPath $OutFile -Value $pick -NoNewline -Encoding ascii
|
||||
Write-Host ""
|
||||
Write-Host " Selected: $pick"
|
||||
Start-Sleep -Seconds 1
|
||||
exit 0
|
||||
}
|
||||
}
|
||||
'Escape' { exit 1 }
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,11 @@ set "BIOSDIR=%~dp0"
|
||||
set "FLASH=%BIOSDIR%Flash64W.exe"
|
||||
set "MANIFEST=%BIOSDIR%models.txt"
|
||||
|
||||
REM --- Report this machine to the PXE webapp (Hardware Coverage) ---
|
||||
REM Every WinPE boot maps this share, whatever image is picked next, so this
|
||||
REM is the one place that sees standard/engineer builds too. Best effort.
|
||||
if exist "%BIOSDIR%hardware-report.ps1" powershell -NoProfile -ExecutionPolicy Bypass -File "%BIOSDIR%hardware-report.ps1" >NUL 2>&1
|
||||
|
||||
if exist "%FLASH%" goto :flash_ok
|
||||
echo Flash64W.exe not found, skipping BIOS check.
|
||||
set "BIOS_STATUS=Skipped (Flash64W.exe missing)"
|
||||
|
||||
58
playbook/shopfloor-setup/BIOS/hardware-report.ps1
Normal file
58
playbook/shopfloor-setup/BIOS/hardware-report.ps1
Normal file
@@ -0,0 +1,58 @@
|
||||
# hardware-report.ps1 - Report this machine's hardware to the PXE webapp so
|
||||
# its Hardware Coverage page can flag models with no driver pack or BIOS.
|
||||
#
|
||||
# Called by check-bios.cmd, which every WinPE boot runs from the BIOS share
|
||||
# before the imaging menu, so it reports every boot - including ones that
|
||||
# never pick an image - rather than only those that reach the Y: status push.
|
||||
#
|
||||
# Sends the exact WMI model PESetup matches driver packs on, the Dell
|
||||
# system SKU (what Dell's catalogs key on) and the BIOS version. Runs in
|
||||
# WinPE's stripped PowerShell: wmic only, no CIM cmdlets. Best effort:
|
||||
# logs to X:\Windows\Temp\hardware-report.log and never fails the boot.
|
||||
|
||||
param(
|
||||
[string]$PxeServer = $(if ($env:PXESERVER) { $env:PXESERVER } else { '172.16.9.1' }),
|
||||
[int]$Port = 9009,
|
||||
[int]$TimeoutSec = 5
|
||||
)
|
||||
|
||||
$logFile = 'X:\Windows\Temp\hardware-report.log'
|
||||
function Log {
|
||||
param([string]$msg)
|
||||
try { "$(Get-Date -Format s) $msg" | Out-File -FilePath $logFile -Append -Encoding utf8 } catch { }
|
||||
}
|
||||
|
||||
function Get-WmicValue {
|
||||
param([string]$Class, [string]$Property)
|
||||
try {
|
||||
$line = (& wmic $Class get $Property /value 2>$null |
|
||||
Where-Object { $_ -match '=' } | Select-Object -First 1)
|
||||
if ($line) { return ($line -split '=', 2)[1].Trim() }
|
||||
} catch { Log "wmic $Class $Property exception: $_" }
|
||||
return ''
|
||||
}
|
||||
|
||||
$hw = @{
|
||||
serial = Get-WmicValue 'bios' 'SerialNumber'
|
||||
model = Get-WmicValue 'computersystem' 'Model'
|
||||
manufacturer = Get-WmicValue 'computersystem' 'Manufacturer'
|
||||
sku = Get-WmicValue 'computersystem' 'SystemSKUNumber'
|
||||
bios_version = Get-WmicValue 'bios' 'SMBIOSBIOSVersion'
|
||||
}
|
||||
Log "model=$($hw.model) sku=$($hw.sku) bios=$($hw.bios_version) serial=$($hw.serial)"
|
||||
|
||||
if (-not $hw.model) {
|
||||
Log "no model - nothing to report"
|
||||
exit 0
|
||||
}
|
||||
|
||||
try {
|
||||
$resp = Invoke-WebRequest -Uri "http://${PxeServer}:${Port}/hardware/seen" -Method POST `
|
||||
-Body ($hw | ConvertTo-Json -Compress) -ContentType 'application/json' `
|
||||
-UseBasicParsing -TimeoutSec $TimeoutSec -ErrorAction Stop
|
||||
Log "OK http=$($resp.StatusCode)"
|
||||
} catch {
|
||||
Log "ERR $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,51 +1,73 @@
|
||||
# ModelSubstring|BIOSFile|Version
|
||||
13 Plus PB13250|Dell_Pro_PA13250_PA14250_PB13250_PB14250_PB16250_2.8.1.exe|2.8.1
|
||||
14 MC14250|Dell_Pro_Max_MC16250_MC14250_1.9.0.exe|1.9.0
|
||||
14 PC14250|Dell_Pro_PC14250_PC16250_1.7.0.exe|1.7.0
|
||||
14 Premium MA14250|Dell_Pro_Max_MA14250_MA16250_1.7.1.exe|1.7.1
|
||||
16 Plus MB16250|Dell_Pro_Max_MB16250_MB18250_2.2.2.exe|2.2.2
|
||||
24250|Dell_Pro_Plus_QB24250_QC24250_QC24251_1.9.2.exe|1.9.2
|
||||
5430|Latitude_5430_7330_Rugged_1.41.0.exe|1.41.0
|
||||
7080|OptiPlex_7080_1.37.0.exe|1.37.0
|
||||
7090|OptiPlex_7090_1.40.0.exe|1.40.0
|
||||
7220 Rugged|Latitude_7220_Rugged_Extreme_Tablet_1.50.0.exe|1.50.0
|
||||
7230 Rugged Extreme Tablet|Latitude_7230_1.30.1.exe|1.30.1
|
||||
7320 2-in-1 Detachable|Dell_Latitude_7320_Detachable_1.45.0_64.exe|1.45.0
|
||||
7350 Detachable|Latitude_7350_Detachable_1.9.1.exe|1.9.1
|
||||
7400 AIO|Latitude_7X00_1.43.0.exe|1.43.0
|
||||
AIO Plus 7410|Latitude_7X10_1.43.0.exe|1.43.0
|
||||
AIO Plus 7420|Latitude_7X20_1.48.0.exe|1.48.0
|
||||
Latitude 5330|Latitude_5330_1.33.0.exe|1.33.0
|
||||
Latitude 5340|Latitude_5340_1.27.1.exe|1.27.1
|
||||
Latitude 5350|Latitude_5350_1.20.0.exe|1.20.0
|
||||
Latitude 5440|Latitude_5440_Precision_3480_1.28.1.exe|1.28.1
|
||||
Latitude 5450|Latitude_5450_Precision_3490_1.20.1.exe|1.20.1
|
||||
Latitude 5530|Precision_5530_1.42.0.exe|1.42.0
|
||||
Latitude 5540|Precision_5540_1.42.0.exe|1.42.0
|
||||
Latitude 7430|Latitude_7X30_1.38.0.exe|1.38.0
|
||||
Latitude 7440|Latitude_7X40_1.28.1.exe|1.28.1
|
||||
Latitude 7450|OptiPlex_7450_1.34.0.exe|1.34.0
|
||||
Micro 7010|OptiPlex_7010_1.34.0_SEMB.exe|1.34.0
|
||||
SFF Plus 7010|OptiPlex_7010_1.34.0_SEMB.exe|1.34.0
|
||||
Tower Plus 7010|OptiPlex_7010_1.34.0_SEMB.exe|1.34.0
|
||||
Micro QCM1250|Dell_Pro_QBT1250_QBS1250_QBM1250_QCT1250_QCS1250_QCM1250_SEMB_1.12.2.exe|1.12.2
|
||||
OptiPlex 3000|OptiPlex_3000_1.38.0.exe|1.38.0
|
||||
OptiPlex 7000|OptiPlex_7000_1.38.0.exe|1.38.0
|
||||
Precision 5490|OptiPlex_5490_AIO_1.45.0.exe|1.45.0
|
||||
Precision 5550|Precision_3590_3591_Latitude_5550_1.8.0.exe|1.8.0
|
||||
Precision 5570|XPS9520_Precision5570_1.39.0_QSL0.exe|1.39.0
|
||||
Precision 5680|Precision_5680_1_27_0.exe|1.27.0
|
||||
Precision 5690|Precision_5690_1.18.0.exe|1.18.0
|
||||
Precision 5820 Tower|Precision_5820_2.48.0.exe|2.48.0
|
||||
Precision 5860 Tower|Precision_5860_3.5.0.exe|3.5.0
|
||||
Precision 7560|Precision_7X60_1.44.1.exe|1.44.1
|
||||
Precision 7670|Precision_7X70_1.8.0.exe|1.8.0
|
||||
Precision 7680|Precision_7X80_1.9.0.exe|1.9.0
|
||||
Precision 7770|OptiPlex_7770_7470_1.40.0.exe|1.40.0
|
||||
Precision 7780|OptiPlex_7780_7480_1.43.0.exe|1.43.0
|
||||
Precision 7820 Tower|Precision_7820_7920_2.50.0.exe|2.50.0
|
||||
Precision 7865 Tower|Precision_7865_1.6.1.exe|1.6.1
|
||||
Precision 7875 Tower|Precision_7875_SHP_02.07.03.exe|2.7.3
|
||||
Rugged 14 RB14250|Dell_Pro_Rugged_RB14250_RA13250_1.13.1.exe|1.13.1
|
||||
Tower Plus 7020|OptiPlex_7020_1.22.1_SEMB.exe|1.22.1
|
||||
Tower Plus QBT1250|Dell_Pro_QBT1250_QBS1250_QBM1250_QCT1250_QCS1250_QCM1250_SEMB_1.12.2.exe|1.12.2
|
||||
13 Plus PB13250|Dell_Pro_PA13250_PA14250_PB13250_PB14250_PB16250_2.18.1.exe|2.18.1
|
||||
14 MC14250|Dell_Pro_Max_MC16250_MC14250_1.19.2.exe|1.19.2
|
||||
14 PC14250|Dell_Pro_PC14250_PC16250_1.18.0.exe|1.18.0
|
||||
14 Premium MA14250|Dell_Pro_Max_MA14250_MA16250_1.13.1.exe|1.13.1
|
||||
16 Plus MB16250|Dell_Pro_Max_MB16250_MB18250_3.3.2.exe|3.3.2
|
||||
QB24250|Dell_Pro_Plus_QB24250_QC24250_QC24251_Combine_1.18.0.exe|1.18.0
|
||||
QC24250|Dell_Pro_Plus_QB24250_QC24250_QC24251_Combine_1.18.0.exe|1.18.0
|
||||
QC24251|Dell_Pro_Plus_QB24250_QC24250_QC24251_Combine_1.18.0.exe|1.18.0
|
||||
5430 Rugged|Latitude_5430_7330_Rugged_1.45.0.exe|1.45.0
|
||||
7330 Rugged|Latitude_5430_7330_Rugged_1.45.0.exe|1.45.0
|
||||
Latitude 5430|Latitude_5430_1.41.0.exe|1.41.0
|
||||
7080|OptiPlex_7080_1.39.0.exe|1.39.0
|
||||
7090 Ultra|OptiPlex_7090_UFF_1.44.0.exe|1.44.0
|
||||
7090|OptiPlex_7090_1.44.0.exe|1.44.0
|
||||
7220 Rugged|Latitude_7220_Rugged_Extreme_Tablet_1.53.0.exe|1.53.0
|
||||
7230 Rugged Extreme Tablet|Latitude_7230_1.34.0.exe|1.34.0
|
||||
7320 Detachable|Dell_Latitude_7320_Detachable_1.49.0_64.exe|1.49.0
|
||||
7350 Detachable|Latitude_7350_Detachable_1.24.0.exe|1.24.0
|
||||
7400 AIO|OptiPlex_7400_1.1.61.exe|1.1.61
|
||||
7400 All-in-One|OptiPlex_7400_1.1.61.exe|1.1.61
|
||||
AIO Plus 7410|OptiPlex_7410_1.37.0.exe|1.37.0
|
||||
AIO Plus 7420|OptiPlex_7420_1.27.0.exe|1.27.0
|
||||
Latitude 5330|Latitude_5330_1.38.0.exe|1.38.0
|
||||
Latitude 5340|Latitude_5340_1.31.0.exe|1.31.0
|
||||
Latitude 5350|Latitude_5350_1.26.0.exe|1.26.0
|
||||
Latitude 5440|Latitude_5440_Precision_3480_1.34.0.exe|1.34.0
|
||||
Latitude 5450|Latitude_5450_Precision_3490_1.26.0.exe|1.26.0
|
||||
Latitude 5530|Precision_3570_Latitude_5530_1.38.0.exe|1.38.0
|
||||
Latitude 5540|Precision_3580_Latitude_5540_1.31.0.exe|1.31.0
|
||||
Latitude 7430|Latitude_7X30_1.42.0.exe|1.42.0
|
||||
Latitude 7440|Latitude_7X40_1.34.0.exe|1.34.0
|
||||
Latitude 7450|Latitude_7X50_1.26.0.exe|1.26.0
|
||||
Micro 7010|OptiPlex_7010_1.38.0_SEMB.exe|1.38.0
|
||||
SFF Plus 7010|OptiPlex_7010_1.38.0_SEMB.exe|1.38.0
|
||||
Tower Plus 7010|OptiPlex_7010_1.38.0_SEMB.exe|1.38.0
|
||||
Micro 7020|OptiPlex_7020_1.28.1_SEMB.exe|1.28.1
|
||||
Micro Plus 7020|OptiPlex_7020_1.28.1_SEMB.exe|1.28.1
|
||||
SFF 7020|OptiPlex_7020_1.28.1_SEMB.exe|1.28.1
|
||||
SFF Plus 7020|OptiPlex_7020_1.28.1_SEMB.exe|1.28.1
|
||||
Tower 7020|OptiPlex_7020_1.28.1_SEMB.exe|1.28.1
|
||||
Tower Plus 7020|OptiPlex_7020_1.28.1_SEMB.exe|1.28.1
|
||||
Micro QCM1250|Dell_Pro_QBT1250_QBS1250_QBM1250_QCT1250_QCS1250_QCM1250_SEMB_1.19.3.exe|1.19.3
|
||||
Tower Plus QBT1250|Dell_Pro_QBT1250_QBS1250_QBM1250_QCT1250_QCS1250_QCM1250_SEMB_1.19.3.exe|1.19.3
|
||||
OptiPlex 3000 Thin Client|OptiPlex_TC_1.37.0.exe|1.37.0
|
||||
OptiPlex 3000|OptiPlex_3000_1.42.0.exe|1.42.0
|
||||
OptiPlex 7000|OptiPlex_7000_1.42.0.exe|1.42.0
|
||||
Precision 5490|Precision_5490_1.24.0.exe|1.24.0
|
||||
Precision 5550|Precision_5550_1.42.0.exe|1.42.0
|
||||
Precision 5570|XPS9520_Precision5570_1.42.0_QSL0.exe|1.42.0
|
||||
Precision 5680|Precision_5680_1_29_0.exe|1.29.0
|
||||
Precision 5690|Precision_5690_1.26.0.exe|1.26.0
|
||||
Precision 5820 Tower|Precision_5820_2.50.2.exe|2.50.2
|
||||
Precision 5860 Tower|Precision_5860_3.10.0.exe|3.10.0
|
||||
Precision 7560|Precision_7X60_1.49.0.exe|1.49.0
|
||||
Precision 7670|Precision_7x70_1.40.0.exe|1.40.0
|
||||
Precision 7680|Precision_7X80_1.32.0.exe|1.32.0
|
||||
Precision 7770|Precision_7x70_1.40.0.exe|1.40.0
|
||||
Precision 7780|Precision_7X80_1.32.0.exe|1.32.0
|
||||
Precision 7820 Tower|Precision_7820_7920_2.53.1.exe|2.53.1
|
||||
Precision 7865 Tower|Precision_7865_1.29.0.exe|1.29.0
|
||||
Precision 7875 Tower|Precision_7875_SHP_02.12.00.exe|2.12.0
|
||||
Rugged 14 RB14250|Dell_Pro_Rugged_RB14250_RA13250_1.19.1.exe|1.19.1
|
||||
P314260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
P316260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
P514260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
P516260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
P713260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
P703260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
P714260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
P704260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
PW514260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
PW516260|Dell_Pro_P31x260_P51x260_P7xx260_Dell_Pro_Precision_PW51x260_2.4.2.exe|2.4.2
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
# Register-BulkEnrollTask-Standard.ps1 - register the BulkEnrollmentOrchestrator
|
||||
# logon task for the gea-standard and gea-engineer images.
|
||||
#
|
||||
# WHY THIS IS A FILE AND NOT AN INLINE COMMAND
|
||||
# This ran inline as "powershell.exe -Command ..." in the unattend's specialize
|
||||
# pass at RunSynchronousCommand Order 39. RunSynchronousCommand/Path is capped at
|
||||
# 259 characters by the unattend schema; the inline form was 604, so Windows
|
||||
# Setup rejected the ENTIRE answer file:
|
||||
#
|
||||
# /settings/RunSynchronous/RunSynchronousCommand/[Order="39"]/Path
|
||||
# Description = Value is invalid. hrResult = 0x80220005 pass = specialize
|
||||
#
|
||||
# One over-length value invalidates every pass, so the machine stops at a dialog
|
||||
# with nothing configured.
|
||||
#
|
||||
# WHY THERE ARE TWO OF THESE SCRIPTS
|
||||
# gea-shopfloor uses Register-BulkEnrollTask.ps1, which launches the orchestrator
|
||||
# as a PowerShell script with -ManualFallback: shopfloor bays are vanilla by
|
||||
# design, so the orchestrator skips BPRT injection and the provisioning package.
|
||||
# gea-standard and gea-engineer enrol normally and call the .cmd wrapper instead.
|
||||
# All three image types share ONE Deploy\Applications\BPRT directory (a symlink
|
||||
# to _shared\Applications\BPRT), so the two variants need distinct filenames.
|
||||
#
|
||||
# PESetup's CopyPackages copies the whole of <media>\Deploy\Applications to the
|
||||
# target before the image is applied, so this script is present at
|
||||
# C:\Deploy\Applications\BPRT\ by the time specialize runs. See
|
||||
# docs/PESETUP-INTERNALS.md.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$taskName = 'BulkEnrollmentOrchestrator'
|
||||
$orchestrator = 'C:\Deploy\Applications\BPRT\Start-BulkEnrollOrchestrator.cmd'
|
||||
|
||||
# Self-deleting task: schtasks /Delete drops the registration, then the
|
||||
# orchestrator runs. cmd's & chains them so the delete happens even if the
|
||||
# orchestrator later reboots the machine.
|
||||
$argument = '/c schtasks /Delete /TN {0} /F & "{1}"' -f $taskName, $orchestrator
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument $argument
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
# S-1-5-32-544 = BUILTIN\Administrators. GroupId + Highest so it runs elevated
|
||||
# for whichever admin logs on first, without naming an account.
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-544' -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 3)
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
|
||||
|
||||
Write-Host "Registered scheduled task '$taskName'."
|
||||
42
playbook/shopfloor-setup/BPRT/Register-BulkEnrollTask.ps1
Normal file
42
playbook/shopfloor-setup/BPRT/Register-BulkEnrollTask.ps1
Normal file
@@ -0,0 +1,42 @@
|
||||
# Register-BulkEnrollTask.ps1 - register the BulkEnrollmentOrchestrator logon task.
|
||||
#
|
||||
# WHY THIS IS A FILE AND NOT AN INLINE COMMAND
|
||||
# This ran as an inline "powershell.exe -Command ..." in the unattend's
|
||||
# specialize pass, at RunSynchronousCommand Order 16. The unattend schema caps
|
||||
# RunSynchronousCommand/Path at 259 characters; the inline form had grown to 676,
|
||||
# so Windows Setup rejected the WHOLE answer file:
|
||||
#
|
||||
# /settings/RunSynchronous/RunSynchronousCommand/[Order="16"]/Path
|
||||
# Description = Value is invalid. hrResult = 0x80220005 pass = specialize
|
||||
# "Windows could not parse or process unattend answer file ... The answer file
|
||||
# is invalid."
|
||||
#
|
||||
# One over-length value blocks every pass, so the machine stops at a dialog with
|
||||
# nothing configured. Every shopfloor build from 2026-08-05 evening onward failed
|
||||
# this way. Keep the unattend's Path short and put logic here.
|
||||
#
|
||||
# PESetup's CopyPackages copies the whole of <media>\Deploy\Applications to the
|
||||
# target before the image is applied, so this script is present at
|
||||
# C:\Deploy\Applications\BPRT\ by the time specialize runs. See
|
||||
# docs/PESETUP-INTERNALS.md.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
$taskName = 'BulkEnrollmentOrchestrator'
|
||||
$orchestrator = 'C:\Deploy\Applications\BPRT\Start-BulkEnrollOrchestrator.ps1'
|
||||
|
||||
# Self-deleting task: schtasks /Delete removes the registration, then the
|
||||
# orchestrator runs. cmd's & chains them so the delete happens even if the
|
||||
# orchestrator later reboots the machine.
|
||||
$argument = '/c schtasks /Delete /TN {0} /F & powershell.exe -NoProfile -ExecutionPolicy Bypass -File "{1}" -ManualFallback' -f $taskName, $orchestrator
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument $argument
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
# S-1-5-32-544 = BUILTIN\Administrators. GroupId + Highest so it runs elevated
|
||||
# for whichever admin logs on first, without naming an account.
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-544' -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -ExecutionTimeLimit (New-TimeSpan -Hours 3)
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
|
||||
|
||||
Write-Host "Registered scheduled task '$taskName'."
|
||||
84
playbook/shopfloor-setup/BPRT/Select-PCConfig.ps1
Normal file
84
playbook/shopfloor-setup/BPRT/Select-PCConfig.ps1
Normal file
@@ -0,0 +1,84 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Return the tenant + PC purpose, pre-seeded if WinPE already asked.
|
||||
|
||||
.DESCRIPTION
|
||||
Start-BulkEnrollOrchestrator.ps1 calls this at first logon and captures
|
||||
stdout:
|
||||
|
||||
$pcType = powershell.exe -File 'C:\Deploy\Applications\BPRT\Select-PCConfig.ps1' -LogoPath '...'
|
||||
|
||||
It uses the answer twice - to pick which MCL package to apply, and to select
|
||||
which bulk token Insert-BPRTToPPKG.ps1 injects (it matches on Tenant AND
|
||||
Purpose). The vendor's version asks with a GUI, which means an otherwise
|
||||
unattended build stops at a dialog until somebody walks over.
|
||||
|
||||
startnet.cmd now asks the same question at the PXE menu, while the tech who
|
||||
knows what the machine is for is standing at it, and writes the answer to
|
||||
C:\Enrollment\pc-config.txt. This shim returns that value when present and
|
||||
falls through to the vendor dialog when it is not - so nothing is lost if the
|
||||
tech picks "ask at first logon", or on a bay imaged before this existed.
|
||||
|
||||
ONLY the choice is pre-seeded. Token injection still happens at first logon:
|
||||
Insert-BPRTToPPKG.ps1 fetches an encrypted token table from
|
||||
mcl.dwcdn.geaerospace.com, which is unreachable from the isolated imaging
|
||||
LAN.
|
||||
|
||||
.OUTPUTS
|
||||
"{Tenant}_{PCType}" - Tenant GCCH|RoW, PCType PERS|SH|LOAN.
|
||||
Identical contract to the vendor script.
|
||||
|
||||
.NOTES
|
||||
The vendor script is kept alongside as Select-PCConfig-vendor.ps1. Replace
|
||||
THAT file when the vendor ships a new one; this shim only needs changing if
|
||||
the return contract changes.
|
||||
#>
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory = $false)]
|
||||
[string]$LogoPath
|
||||
)
|
||||
|
||||
$seedFile = 'C:\Enrollment\pc-config.txt'
|
||||
$vendor = Join-Path $PSScriptRoot 'Select-PCConfig-vendor.ps1'
|
||||
$logDir = 'C:\Logs\BPRT\Orchestrator'
|
||||
$shimLog = Join-Path $logDir 'Select-PCConfig-shim.log'
|
||||
|
||||
function Log {
|
||||
param([string]$Message)
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
Add-Content -Path $shimLog -Value ("{0} {1}" -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message)
|
||||
} catch { }
|
||||
}
|
||||
|
||||
# Valid values, so a typo in the seed file falls back to the dialog instead of
|
||||
# handing the orchestrator a string that matches no token and no package.
|
||||
$validTenants = @('GCCH', 'RoW')
|
||||
$validPurposes = @('PERS', 'SH', 'LOAN')
|
||||
|
||||
if (Test-Path $seedFile) {
|
||||
$seed = (Get-Content $seedFile -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($seed) { $seed = $seed.Trim() }
|
||||
$parts = if ($seed) { $seed.Split('_') } else { @() }
|
||||
|
||||
if ($parts.Count -eq 2 -and $validTenants -contains $parts[0] -and $validPurposes -contains $parts[1]) {
|
||||
Log "Pre-seeded value '$seed' from $seedFile - dialog skipped."
|
||||
Write-Output $seed
|
||||
exit 0
|
||||
}
|
||||
|
||||
Log "Ignoring malformed pre-seed '$seed' in $seedFile; falling back to the dialog."
|
||||
}
|
||||
|
||||
if (Test-Path $vendor) {
|
||||
Log "No usable pre-seed; showing the vendor dialog."
|
||||
if ($LogoPath) { & $vendor -LogoPath $LogoPath }
|
||||
else { & $vendor }
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Neither a seed nor the vendor dialog. Say so in the log rather than returning
|
||||
# something invented - the orchestrator would inject the wrong token.
|
||||
Log "ERROR: no pre-seed at $seedFile and no vendor script at $vendor."
|
||||
exit 1
|
||||
@@ -6,7 +6,7 @@
|
||||
# (Y:) early, then idles for many minutes while the full Windows image applies.
|
||||
# Samba's `deadtime` drops idle sessions, so by the time WinPE reached the
|
||||
# copies the Y: mount was dead and most copies failed (symptom: a bay with
|
||||
# only site-config.json staged, then nothing). Doing the bulk copy here - at
|
||||
# almost nothing staged). Doing the bulk copy here - at
|
||||
# first logon, in full Windows, on a FRESH share mount with no prior idle -
|
||||
# sidesteps that entirely.
|
||||
#
|
||||
@@ -18,12 +18,9 @@
|
||||
#
|
||||
# WHAT IT FETCHES (generic bulk - Phase 1)
|
||||
# \\<server>\enrollment\shopfloor-setup\Run-ShopfloorSetup.ps1 -> C:\Enrollment\
|
||||
# \\<server>\enrollment\shopfloor-setup\{backup_lockdown.bat,Shopfloor,common,
|
||||
# _ntlars-backups,gea-shopfloor-<pctype>} -> C:\Enrollment\shopfloor-setup\
|
||||
# \\<server>\enrollment\pre-install\{preinstall.json,installers,udc-backups}
|
||||
# \\<server>\enrollment\shopfloor-setup\Shopfloor -> C:\Enrollment\shopfloor-setup\
|
||||
# \\<server>\enrollment\pre-install\{preinstall.json,postppkg.json,installers}
|
||||
# -> C:\PreInstall\
|
||||
# (Heavy per-type payloads - CMM/Keyence/WaxTrace - are still staged in WinPE
|
||||
# for now; Phase 2 moves those here too.)
|
||||
#
|
||||
# LOGGING
|
||||
# Verbose transcript + a per-item table to C:\Logs\Fetch\. Every robocopy logs
|
||||
@@ -147,21 +144,13 @@ $PIN = 'C:\PreInstall'
|
||||
|
||||
Fetch-Item -Label 'Run-ShopfloorSetup.ps1' -SrcDir 'shopfloor-setup' -DstDir $ENR -Files @('Run-ShopfloorSetup.ps1')
|
||||
# Verify-And-Heal-Staging runs as its own unattend step (right after this Fetch,
|
||||
# before the production-network switch) to re-pull anything that did not arrive -
|
||||
# including the heavy CMM payload Fetch does not carry. Pull the small script
|
||||
# itself here so it is on disk for that step.
|
||||
# before the production-network switch) to re-pull anything that did not arrive.
|
||||
# Pull the small script itself here so it is on disk for that step.
|
||||
Fetch-Item -Label 'Verify-And-Heal-Staging.ps1' -SrcDir 'shopfloor-setup' -DstDir $ENR -Files @('Verify-And-Heal-Staging.ps1')
|
||||
Fetch-Item -Label 'backup_lockdown.bat' -SrcDir 'shopfloor-setup' -DstDir $SFD -Files @('backup_lockdown.bat')
|
||||
Fetch-Item -Label 'Shopfloor baseline' -SrcDir 'shopfloor-setup\Shopfloor' -DstDir (Join-Path $SFD 'Shopfloor') -Recurse
|
||||
Fetch-Item -Label 'common' -SrcDir 'shopfloor-setup\common' -DstDir (Join-Path $SFD 'common') -Recurse
|
||||
Fetch-Item -Label '_ntlars-backups' -SrcDir 'shopfloor-setup\_ntlars-backups' -DstDir (Join-Path $SFD '_ntlars-backups') -Recurse
|
||||
if ($pcType) {
|
||||
Fetch-Item -Label "type:$pcType" -SrcDir "shopfloor-setup\$pcType" -DstDir (Join-Path $SFD $pcType) -Recurse
|
||||
}
|
||||
# preinstall bundle
|
||||
Fetch-Item -Label 'preinstall.json' -SrcDir 'pre-install' -DstDir $PIN -Files @('preinstall.json')
|
||||
# site app lists + their installers (and the PowerShell 7 MSI)
|
||||
Fetch-Item -Label 'app lists' -SrcDir 'pre-install' -DstDir $PIN -Files @('preinstall.json', 'postppkg.json')
|
||||
Fetch-Item -Label 'preinstall installers' -SrcDir 'pre-install\installers' -DstDir (Join-Path $PIN 'installers') -Recurse
|
||||
Fetch-Item -Label 'udc-backups' -SrcDir 'pre-install\udc-backups' -DstDir (Join-Path $PIN 'udc-backups') -Recurse
|
||||
|
||||
# --- Unmount ---
|
||||
cmd /c "net use $drive /delete /y >nul 2>&1"
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
# Run-ShopfloorSetup.ps1 - Main dispatcher for shopfloor PC type setup.
|
||||
# Run-ShopfloorSetup.ps1 - shopfloor setup (gea-shopfloor-standard).
|
||||
#
|
||||
# Called by the unattend FirstLogonCommands as SupportUser (admin), after
|
||||
# Order 10 has already applied the PPKG (run-enrollment.ps1).
|
||||
# Flow:
|
||||
# 1. PreInstall apps (Oracle, VC++, OpenText, UDC) -- from local bundle
|
||||
# 2. Type-specific scripts (eDNC, ACLs, CMM share apps, etc.)
|
||||
# 3. Deferred baseline (desktop org, taskbar pins)
|
||||
# 4. Copy desktop tools (sync_intune, Configure-PC, Set-MachineNumber)
|
||||
# 5. Run enrollment (PPKG install + wait for completion)
|
||||
# 6. Register sync_intune as @logon scheduled task
|
||||
# 7. Reboot -- PPKG file operations complete, sync_intune fires on next logon
|
||||
#
|
||||
# Called by the unattend FirstLogonCommands as SupportUser (admin).
|
||||
# 1. Site app list "during setup": C:\PreInstall\preinstall.json
|
||||
# (Shopfloor\Install-AppList.ps1; empty in the generic build)
|
||||
# 2. Copy desktop tools (sync_intune, Force-Lockdown, SetShopfloorAutoLogon)
|
||||
# 3. Register the "Shopfloor Intune Sync" logon task (Monitor-IntuneProgress:
|
||||
# waits for the PPKG/Intune lockdown, then hands the PC to ShopFloor)
|
||||
# 4. Register "GE Shopfloor Post-PPKG" (SYSTEM, at startup): installs
|
||||
# C:\PreInstall\postppkg.json once after the reboot (empty in the
|
||||
# generic build) and clears AutoLogonCount after the ShopFloor handover
|
||||
# 5. Network gate, run-enrollment (a no-op when Order 10 applied the PPKG),
|
||||
# then Monitor-IntuneProgress -PostPpkg: settle and reboot
|
||||
|
||||
# --- Transcript logging ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
@@ -62,7 +65,7 @@ cmd /c "shutdown /a 2>nul" *>$null
|
||||
# done work and continues from where it left off.
|
||||
#
|
||||
# Also top up AutoLogonCount so the SupportUser autologon budget
|
||||
# (LogonCount=7 from unattend XML) survives extra unplanned reboots.
|
||||
# (LogonCount=12 from unattend XML) survives extra unplanned reboots.
|
||||
$selfResumeKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce'
|
||||
$selfResumeName = 'ResumeRunShopfloorSetup'
|
||||
$selfResumeCmd = 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "' + $PSCommandPath + '"'
|
||||
@@ -94,9 +97,51 @@ $enrollDir = "C:\Enrollment"
|
||||
$typeFile = Join-Path $enrollDir "pc-type.txt"
|
||||
$setupDir = Join-Path $enrollDir "shopfloor-setup"
|
||||
|
||||
# NOTE: there is deliberately NO wait for an Entra join here - but NOT because
|
||||
# shopfloor bays skip enrollment. They do enrol: the SFLD provisioning package
|
||||
# joins Entra using the BPRT token it carries, and a human then assigns the
|
||||
# device category in Intune.
|
||||
#
|
||||
# The wait is absent because the join cannot happen yet. At this point the bay is
|
||||
# still on the isolated PXE LAN with no route to Entra - observed on 579C144
|
||||
# 2026-08-06, holding 172.16.9.81 and 172.24.19.142, neither in the production
|
||||
# ranges. sync_intune retries every 30 seconds until the tech re-cables to
|
||||
# production, which is the right place to wait. An earlier version blocked here
|
||||
# for 45 minutes and then warned about a failure that had not happened.
|
||||
# "Entra ID Joined: false" in C:\Logs\BPRT\criticalChecks.json straight after
|
||||
# imaging is therefore NORMAL, not a fault.
|
||||
#
|
||||
# CORRECTION (2026-08-06): this comment previously claimed shopfloor PCs are
|
||||
# "vanilla by design" and that the orchestrator runs with -ManualFallback to skip
|
||||
# BPRT injection and the package entirely. That was wrong and dangerous.
|
||||
# -ManualFallback runs sysprep /oobe /reboot, so wiring it to an at-logon task
|
||||
# syspreped finished machines seconds after autologon and destroyed the
|
||||
# deployment chain. See docs/OWNERSHIP.md.
|
||||
|
||||
if (-not (Test-Path $typeFile)) {
|
||||
Write-Host "No pc-type.txt found - skipping shopfloor setup."
|
||||
exit 0
|
||||
# A missing pc-type.txt means one of two very different things, and the old
|
||||
# blanket "skip + exit 0" hid the bad one for weeks: four Display bays sat
|
||||
# at imaging stage 2 with a green exit code and nobody noticed.
|
||||
# - no C:\Enrollment at all -> this machine was never staged by WinPE
|
||||
# (pre-imaging, or the staging block never ran). That is a FAILURE on a
|
||||
# machine that has clearly just been imaged, so say so loudly.
|
||||
# - C:\Enrollment exists but no pc-type.txt -> staging ran and the write
|
||||
# failed. Also a failure.
|
||||
$stagingLog = Join-Path $enrollDir 'winpe-staging.log'
|
||||
$detail = if (Test-Path $enrollDir) {
|
||||
"C:\Enrollment exists but pc-type.txt is missing - WinPE staging ran but did not write it. Check $stagingLog."
|
||||
} else {
|
||||
"C:\Enrollment does not exist - WinPE staging never ran. The Windows volume was probably not found in startnet.cmd, so pc-type.txt, the enrollment package and shopfloor-setup were ALL skipped."
|
||||
}
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host " FAILED: no pc-type.txt at $typeFile"
|
||||
Write-Host " $detail"
|
||||
Write-Host " Shopfloor setup cannot run. This PC is imaged but NOT configured."
|
||||
Write-Host "================================================================"
|
||||
Write-Host ""
|
||||
Report-Stage -Stage 'Run-ShopfloorSetup: FAILED - no pc-type.txt' -Index 2 -Status 'failed' -Error_ $detail
|
||||
exit 1
|
||||
}
|
||||
|
||||
$pcType = (Get-Content $typeFile -First 1).Trim()
|
||||
@@ -105,149 +150,27 @@ if (-not $pcType) {
|
||||
exit 0
|
||||
}
|
||||
|
||||
$subtypeFile = Join-Path $enrollDir "pc-subtype.txt"
|
||||
$pcSubType = ''
|
||||
if (Test-Path $subtypeFile) {
|
||||
$pcSubType = (Get-Content $subtypeFile -First 1).Trim()
|
||||
}
|
||||
Write-Host "Shopfloor PC Type: $pcType"
|
||||
|
||||
Write-Host "Shopfloor PC Type: $pcType$(if ($pcSubType) { " / $pcSubType" })"
|
||||
|
||||
# Scripts to skip in the alphabetical baseline loop. Each is either run
|
||||
# explicitly in the finalization phase below, or invoked internally by
|
||||
# another script:
|
||||
#
|
||||
# 05 - Office shortcuts. Invoked by 06 as Phase 0. Office isn't installed
|
||||
# until after the PPKG + reboot, so 05 no-ops initially. 06's SYSTEM
|
||||
# logon task re-runs it on the next boot.
|
||||
# 06 - Desktop org. Phase 2 needs eDNC/NTLARS on disk (installed by
|
||||
# type-specific 01-eDNC.ps1). Run in finalization phase.
|
||||
# 07 - Taskbar pin layout. Reads 06's output. Run in finalization phase.
|
||||
# 08 - Edge default browser + startup tabs. Invoked by 06 as Phase 4.
|
||||
# Reads .url files delivered by DSC (after Intune enrollment). 06's
|
||||
# SYSTEM logon task re-runs it to pick them up.
|
||||
$skipInBaseline = @(
|
||||
'05-OfficeShortcuts.ps1',
|
||||
'06-OrganizeDesktop.ps1',
|
||||
'07-TaskbarLayout.ps1',
|
||||
'08-EdgeDefaultBrowser.ps1',
|
||||
# Machine number flow: split into two scripts registered as scheduled
|
||||
# tasks by Register-CheckMachineNumberTask.ps1. Prompt runs as the
|
||||
# logged-in user (GUI), Apply runs as SYSTEM (privileged writes).
|
||||
# Neither should run in baseline pass.
|
||||
'Prompt-MachineNumber.ps1',
|
||||
'Apply-MachineNumber.ps1',
|
||||
'Check-MachineNumber.ps1',
|
||||
'Configure-PC.ps1'
|
||||
)
|
||||
|
||||
# Scripts run AFTER type-specific scripts complete. 05 and 08 are NOT
|
||||
# here because 06 calls them internally as sub-phases.
|
||||
$runAfterTypeSpecific = @(
|
||||
'06-OrganizeDesktop.ps1',
|
||||
'07-TaskbarLayout.ps1'
|
||||
)
|
||||
|
||||
# --- Run Shopfloor baseline scripts first (skipping deferred ones) ---
|
||||
$baselineDir = Join-Path $setupDir "Shopfloor"
|
||||
if (Test-Path $baselineDir) {
|
||||
$scripts = Get-ChildItem -Path $baselineDir -Filter "*.ps1" -File | Sort-Object Name
|
||||
foreach ($script in $scripts) {
|
||||
if ($skipInBaseline -contains $script.Name) {
|
||||
Write-Host "Skipping baseline: $($script.Name) (runs in finalization phase)"
|
||||
continue
|
||||
}
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
Write-Host "Running baseline: $($script.Name)"
|
||||
try {
|
||||
& $script.FullName
|
||||
} catch {
|
||||
Write-Warning "Baseline script $($script.Name) failed: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- PCType dir alias resolution (2026-05-04 rename reorg) -------------
|
||||
# Fleet PCs may have pc-type.txt = legacy "Standard"/"CMM"/etc OR new
|
||||
# gea-shopfloor-* names. Repo dirs have been renamed to new names; this
|
||||
# helper resolves either form to the actual on-disk dir under $setupDir.
|
||||
$pcTypeDirAliases = @(
|
||||
@('Standard', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
|
||||
@('Standard-Machine', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
|
||||
@('Standard-Timeclock', 'gea-shopfloor-common'),
|
||||
@('CMM', 'gea-shopfloor-cmm'),
|
||||
@('Keyence', 'gea-shopfloor-keyence'),
|
||||
@('Lab', 'gea-shopfloor-common'),
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||
)
|
||||
function Resolve-PCTypeDir {
|
||||
param([string]$BaseDir, [string]$Name)
|
||||
$primary = Join-Path $BaseDir $Name
|
||||
if (Test-Path $primary) { return $primary }
|
||||
foreach ($g in $pcTypeDirAliases) {
|
||||
if ($g -icontains $Name) {
|
||||
foreach ($alias in $g) {
|
||||
if ($alias -ieq $Name) { continue }
|
||||
$candidate = Join-Path $BaseDir $alias
|
||||
if (Test-Path $candidate) { return $candidate }
|
||||
}
|
||||
}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
# --- Run type-specific scripts (if not just baseline Shopfloor) ---
|
||||
if ($pcType -ne "Shopfloor") {
|
||||
$typeDir = Resolve-PCTypeDir -BaseDir $setupDir -Name $pcType
|
||||
if ($typeDir -and (Test-Path $typeDir)) {
|
||||
# Only run numbered scripts (01-eDNC.ps1, 02-MachineNumberACLs.ps1).
|
||||
# Unnumbered .ps1 files (Set-MachineNumber.ps1) are desktop tools,
|
||||
# not installer scripts, and must not auto-run during setup.
|
||||
$scripts = Get-ChildItem -Path $typeDir -Filter "*.ps1" -File |
|
||||
Where-Object { $_.Name -match '^\d' } |
|
||||
Sort-Object Name
|
||||
foreach ($script in $scripts) {
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
Write-Host "Running $pcType setup: $($script.Name)"
|
||||
try {
|
||||
& $script.FullName
|
||||
} catch {
|
||||
Write-Warning "Script $($script.Name) failed: $_"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host "No type-specific scripts found for $pcType."
|
||||
}
|
||||
}
|
||||
|
||||
# --- Finalization: run deferred baseline scripts (desktop org, taskbar pins)
|
||||
# ---
|
||||
# These needed to wait until all apps (eDNC, NTLARS, UDC, OpenText) were
|
||||
# installed by the baseline + type-specific phases above. 06 internally
|
||||
# calls 05 (Office shortcuts) and 08 (Edge config) as sub-phases, so we
|
||||
# only need to invoke 06 and 07 explicitly here.
|
||||
foreach ($name in $runAfterTypeSpecific) {
|
||||
$script = Join-Path $baselineDir $name
|
||||
if (-not (Test-Path $script)) {
|
||||
Write-Warning "Deferred script not found: $script"
|
||||
continue
|
||||
}
|
||||
# --- 1. Site app list, during setup ---
|
||||
Report-Stage -Stage 'Run-ShopfloorSetup: installing the site app list' -Index 3
|
||||
$appList = Join-Path $setupDir 'Shopfloor\Install-AppList.ps1'
|
||||
if (Test-Path -LiteralPath $appList) {
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
Write-Host "Running deferred baseline: $name"
|
||||
try {
|
||||
& $script
|
||||
& $appList -ListPath 'C:\PreInstall\preinstall.json' -Phase 'PreInstall'
|
||||
} catch {
|
||||
Write-Warning "Deferred script $name failed: $_"
|
||||
Write-Warning "Install-AppList failed: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Warning "Install-AppList.ps1 not found at $appList"
|
||||
}
|
||||
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
Report-Stage -Stage 'Run-ShopfloorSetup: site app list done' -Index 4
|
||||
Write-Host "Shopfloor setup complete for $pcType."
|
||||
|
||||
# --- Copy utility scripts to SupportUser desktop ---
|
||||
foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat', 'Force-Lockdown.bat', 'SetShopfloorAutoLogon.bat')) {
|
||||
# --- 2. Copy utility scripts to SupportUser desktop ---
|
||||
foreach ($tool in @('sync_intune.bat', 'Force-Lockdown.bat', 'SetShopfloorAutoLogon.bat')) {
|
||||
$src = Join-Path $setupDir "Shopfloor\$tool"
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$tool" -Force
|
||||
@@ -255,28 +178,7 @@ foreach ($tool in @('sync_intune.bat', 'Configure-PC.bat', 'Force-Lockdown.bat',
|
||||
}
|
||||
}
|
||||
|
||||
# Machine-number-using PC types (collections + nocollections, plus their
|
||||
# legacy Standard-Machine alias) get the Set-MachineNumber helper on the
|
||||
# SupportUser desktop. Timeclock / Lab / common variants don't use a
|
||||
# machine number, so the helper has nothing to do there.
|
||||
$needsMachineNumberHelper = $false
|
||||
if ($pcType -ieq 'Standard' -and $pcSubType -ne 'Timeclock') { $needsMachineNumberHelper = $true }
|
||||
if ($pcType -ieq 'gea-shopfloor-collections' -or $pcType -ieq 'gea-shopfloor-nocollections') { $needsMachineNumberHelper = $true }
|
||||
if ($needsMachineNumberHelper) {
|
||||
$helperSrc = Resolve-PCTypeDir -BaseDir $setupDir -Name 'gea-shopfloor-collections'
|
||||
if (-not $helperSrc) { $helperSrc = Resolve-PCTypeDir -BaseDir $setupDir -Name 'Standard' }
|
||||
foreach ($helper in @('Set-MachineNumber.bat', 'Set-MachineNumber.ps1')) {
|
||||
if ($helperSrc) {
|
||||
$src = Join-Path $helperSrc $helper
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Path $src -Destination "C:\Users\SupportUser\Desktop\$helper" -Force
|
||||
Write-Host "$helper copied to SupportUser desktop."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Register sync_intune as persistent @logon scheduled task ---
|
||||
# --- 3. Register sync_intune as persistent @logon scheduled task ---
|
||||
# Must be registered BEFORE enrollment because Install-ProvisioningPackage
|
||||
# triggers an immediate reboot that kills run-enrollment.ps1. The task
|
||||
# registration must survive the PPKG reboot, so we do it here while
|
||||
@@ -288,13 +190,12 @@ if ($needsMachineNumberHelper) {
|
||||
# PPKG reboot, and is irrelevant to the Intune lifecycle.
|
||||
$taskName = 'Shopfloor Intune Sync'
|
||||
$monitorScript = Join-Path $setupDir 'Shopfloor\lib\Monitor-IntuneProgress.ps1'
|
||||
$configureScript = Join-Path $setupDir 'Shopfloor\Configure-PC.ps1'
|
||||
|
||||
if (Test-Path -LiteralPath $monitorScript) {
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -NoExit -ExecutionPolicy Bypass -File `"$monitorScript`" -AsTask -ConfigureScript `"$configureScript`""
|
||||
-Argument "-NoProfile -NoExit -ExecutionPolicy Bypass -File `"$monitorScript`" -AsTask"
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
@@ -325,94 +226,13 @@ if (Test-Path -LiteralPath $monitorScript) {
|
||||
Write-Warning "Monitor-IntuneProgress.ps1 not found at $monitorScript"
|
||||
}
|
||||
|
||||
# Set auto-logon to expire after 4 more logins (2 needed for sync_intune
|
||||
# pre-reboot + post-reboot, plus 2 margin for unexpected reboots from
|
||||
# Windows Update, PPKG file operations, or script crashes).
|
||||
# (AutoLogonCount intentionally not set -- see comment at top of script)
|
||||
|
||||
# --- Register cross-PC-type enforcers (Acrobat, etc.) ---
|
||||
# These run on every logon regardless of PC type, mounting the SFLD share
|
||||
# for version-pinned app enforcement. Initial install already handled by
|
||||
# preinstall flow; enforcers only kick in when detection fails.
|
||||
# Wired-disable / re-enable dance retired after PXE LAN renumber to
|
||||
# 172.16.9.0/24. GE Report IP filters Get-NetIPAddress on StartsWith("10.")
|
||||
# so PXE LAN addresses are no longer caught - wired NIC can stay up
|
||||
# through the whole imaging chain without leaking to the GE webhook.
|
||||
|
||||
$commonSetupDir = Join-Path $setupDir 'common'
|
||||
|
||||
# --- Register the unified GE-Enforce scheduled task ---
|
||||
# Single dispatcher for all PC-type ongoing-update enforcement. Reads
|
||||
# per-pctype manifest.json from the tsgwp00525 share and processes
|
||||
# common + per-type + per-type-subtype manifests in order.
|
||||
#
|
||||
# Display PCs are excluded: their kiosk user cannot reach the SFLD
|
||||
# share, and everything Display needs (kiosk EXE + Edge policies) is
|
||||
# baked at imaging time (preinstall.json Install-KioskApp + 09-Setup-
|
||||
# Display.ps1). No ongoing share-dependent enforcement on Displays.
|
||||
$noEnforceTypes = @('Display', 'gea-shopfloor-display')
|
||||
$registerGE = Join-Path $commonSetupDir 'Register-GEEnforce.ps1'
|
||||
if ($noEnforceTypes -contains $pcType) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Skipping GE-Enforce registration ($pcType is self-contained) ==="
|
||||
} elseif (Test-Path -LiteralPath $registerGE) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Registering unified GE Shopfloor enforcer ==="
|
||||
try {
|
||||
$enforcerRuntime = Join-Path $commonSetupDir 'GE-Enforce.ps1'
|
||||
$libSource = Join-Path $commonSetupDir 'lib\Install-FromManifest.ps1'
|
||||
# Stage enforcer runtime so the scheduled task can reach it post-imaging.
|
||||
$runtimeDir = 'C:\Program Files\GE\Shopfloor'
|
||||
$runtimeLib = Join-Path $runtimeDir 'lib'
|
||||
foreach ($d in @($runtimeDir, $runtimeLib)) {
|
||||
if (-not (Test-Path $d)) { New-Item -Path $d -ItemType Directory -Force | Out-Null }
|
||||
}
|
||||
Copy-Item -LiteralPath $enforcerRuntime -Destination (Join-Path $runtimeDir 'GE-Enforce.ps1') -Force
|
||||
Copy-Item -LiteralPath $libSource -Destination (Join-Path $runtimeLib 'Install-FromManifest.ps1') -Force
|
||||
& $registerGE -EnforcerPath (Join-Path $runtimeDir 'GE-Enforce.ps1')
|
||||
} catch {
|
||||
Write-Warning "GE-Enforce registration failed: $_"
|
||||
}
|
||||
# --- 4. Post-PPKG app list + AutoLogonCount cleanup (SYSTEM task) ---
|
||||
# Registered before the PPKG step: the PPKG reboot can kill this script.
|
||||
$registerPost = Join-Path $setupDir 'Shopfloor\Register-PostPpkgTask.ps1'
|
||||
if (Test-Path -LiteralPath $registerPost) {
|
||||
try { & $registerPost } catch { Write-Warning "Post-PPKG task registration failed: $_" }
|
||||
} else {
|
||||
Write-Warning "Register-GEEnforce.ps1 not found - no ongoing enforcement will run on this PC"
|
||||
}
|
||||
|
||||
# Map S: drive on user logon for every account in BUILTIN\Users. The
|
||||
# vendor 'SFLD - Consume Credentials' task is principal-restricted and
|
||||
# does not fire for the ShopFloor end-user, so this parallel task fills
|
||||
# the gap. Cross-PC-type because every shopfloor account needs S:.
|
||||
# Display PCs skipped: kiosk user has no SFLD creds, S: map would fail
|
||||
# every logon. Self-contained Display has no share dependency.
|
||||
$registerMapShare = Join-Path $setupDir 'Shopfloor\Register-MapSfldShare.ps1'
|
||||
if ($noEnforceTypes -contains $pcType) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Skipping S: drive logon mapper ($pcType is self-contained) ==="
|
||||
} elseif (Test-Path -LiteralPath $registerMapShare) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Registering S: drive logon mapper ==="
|
||||
try { & $registerMapShare } catch { Write-Warning "Map-SfldShare registration failed: $_" }
|
||||
} else {
|
||||
Write-Host "Register-MapSfldShare.ps1 not found (optional) - skipping"
|
||||
}
|
||||
|
||||
# --- Check Machine Number logon prompt ---
|
||||
# Auto-register the "Check Machine Number" scheduled task. Bays imaged with
|
||||
# the 9999 placeholder will prompt the first ShopFloor end-user logon to
|
||||
# enter the real machine number; on success Update-MachineNumber.ps1 pulls
|
||||
# the per-machine NTLARS .reg + UDC settings JSON + UDC data backup from
|
||||
# SFLD and the task self-unregisters. Self-disables once the number is
|
||||
# real, so safe to always register here.
|
||||
# Skipped for self-contained types (Display) that have no machine number.
|
||||
$registerCheckMN = Join-Path $setupDir 'Shopfloor\Register-CheckMachineNumberTask.ps1'
|
||||
if ($noEnforceTypes -contains $pcType) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Skipping Check Machine Number task ($pcType has no machine number) ==="
|
||||
} elseif (Test-Path -LiteralPath $registerCheckMN) {
|
||||
Write-Host ""
|
||||
Write-Host "=== Registering Check Machine Number logon task ==="
|
||||
try { & $registerCheckMN } catch { Write-Warning "Check-MachineNumber registration failed: $_" }
|
||||
} else {
|
||||
Write-Host "Register-CheckMachineNumberTask.ps1 not found (optional) - skipping"
|
||||
Write-Warning "Register-PostPpkgTask.ps1 not found at $registerPost"
|
||||
}
|
||||
|
||||
# --- Run enrollment (PPKG install) ---
|
||||
|
||||
@@ -1,598 +0,0 @@
|
||||
# 00-PreInstall-MachineApps.ps1 - Install pre-staged apps from C:\PreInstall (Shopfloor baseline)
|
||||
#
|
||||
# Reads C:\PreInstall\preinstall.json (staged by startnet.cmd during WinPE phase) and
|
||||
# installs each app whose PCTypes filter matches the current PC type. Detection runs first
|
||||
# so already-installed apps are skipped. Numbered 00- so it runs before all other Shopfloor
|
||||
# baseline scripts - apps need to exist before later scripts (e.g. 01-eDNC.ps1) reference
|
||||
# them or rely on their state.
|
||||
#
|
||||
# Failure mode: log + continue. Intune DSC (Simple-Install.ps1) is the safety net for any
|
||||
# install that fails here.
|
||||
#
|
||||
# Logs to C:\Logs\PreInstall\install.log (wiped each run).
|
||||
|
||||
$preInstallDir = "C:\PreInstall"
|
||||
$jsonPath = Join-Path $preInstallDir "preinstall.json"
|
||||
$installerDir = Join-Path $preInstallDir "installers"
|
||||
$logDir = "C:\Logs\PreInstall"
|
||||
$logFile = Join-Path $logDir "install.log"
|
||||
|
||||
# --- Inline site-config reader (same pattern as Setup-OpenText.ps1; this script
|
||||
# runs from C:\Enrollment\shopfloor-setup\Shopfloor\ but Get-PCProfile.ps1 may
|
||||
# not be available yet, so keep the lookup self-contained) ---
|
||||
function Get-SiteConfig {
|
||||
$configPath = 'C:\Enrollment\site-config.json'
|
||||
if (-not (Test-Path $configPath)) { return $null }
|
||||
try {
|
||||
return Get-Content $configPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
# --- Setup logging ---
|
||||
# IMPORTANT: do NOT wipe the log on each run. The runner can get killed by an
|
||||
# installer-triggered reboot mid-execution. On the next autologon, the dispatcher
|
||||
# re-runs us - if we wipe the log here, we destroy the forensic record from the
|
||||
# previous (interrupted) run. Instead, append a session header so runs are visible
|
||||
# but history is preserved.
|
||||
if (-not (Test-Path $logDir)) {
|
||||
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-PreInstallLog {
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)]
|
||||
[string]$Message,
|
||||
[Parameter(Position=1)]
|
||||
[ValidateSet('INFO','WARN','ERROR')]
|
||||
[string]$Level = 'INFO'
|
||||
)
|
||||
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$line = "[$stamp] [$Level] $Message"
|
||||
Write-Host $line
|
||||
|
||||
# Synchronous write-through so each line hits disk immediately, even if a system
|
||||
# reboot kills the process right after this call returns. Bypasses the OS write
|
||||
# cache via FileOptions.WriteThrough.
|
||||
try {
|
||||
$fs = New-Object System.IO.FileStream(
|
||||
$logFile,
|
||||
[System.IO.FileMode]::Append,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::Read,
|
||||
4096,
|
||||
[System.IO.FileOptions]::WriteThrough
|
||||
)
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n")
|
||||
$fs.Write($bytes, 0, $bytes.Length)
|
||||
$fs.Flush()
|
||||
$fs.Dispose()
|
||||
}
|
||||
catch {
|
||||
# Last-resort fallback if the FileStream open fails for any reason
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
# Session header so multiple runs are visually distinguishable in the log
|
||||
Write-PreInstallLog "================================================================"
|
||||
Write-PreInstallLog "=== Runner session start (PID $PID) ==="
|
||||
Write-PreInstallLog "================================================================"
|
||||
|
||||
# --- Bail early if no preinstall bundle staged ---
|
||||
if (-not (Test-Path $jsonPath)) {
|
||||
Write-PreInstallLog "No preinstall.json at $jsonPath - skipping (no bundle staged for this PC)."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Read PCTYPE and optional PCSUBTYPE from C:\Enrollment\ ---
|
||||
$pcTypeFile = "C:\Enrollment\pc-type.txt"
|
||||
if (-not (Test-Path $pcTypeFile)) {
|
||||
Write-PreInstallLog "No pc-type.txt at $pcTypeFile - skipping" "WARN"
|
||||
exit 0
|
||||
}
|
||||
$pcType = (Get-Content $pcTypeFile -First 1).Trim()
|
||||
if (-not $pcType) {
|
||||
Write-PreInstallLog "pc-type.txt is empty - skipping" "WARN"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$pcSubtype = ''
|
||||
$subtypeFile = "C:\Enrollment\pc-subtype.txt"
|
||||
if (Test-Path $subtypeFile) {
|
||||
$pcSubtype = (Get-Content $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
$pcProfileKey = if ($pcSubtype) { "$pcType-$pcSubtype" } else { $pcType }
|
||||
Write-PreInstallLog "PC type: $pcType$(if ($pcSubtype) { " (subtype: $pcSubtype, profile: $pcProfileKey)" })"
|
||||
|
||||
# --- Parse JSON ---
|
||||
try {
|
||||
$config = Get-Content $jsonPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-PreInstallLog "Failed to parse $jsonPath : $_" "ERROR"
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $config.Applications) {
|
||||
Write-PreInstallLog "No Applications in preinstall.json"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-PreInstallLog "Staged installer dir: $installerDir"
|
||||
Write-PreInstallLog "Found $($config.Applications.Count) app entries in preinstall.json"
|
||||
|
||||
# --- Site-name override: replace 'West Jefferson' in InstallArgs if
|
||||
# site-config says otherwise. UDC_Setup.exe expects the spaced site
|
||||
# name in quotes as arg 1, with a dash-prefixed machine number as arg 2.
|
||||
$siteConfig = Get-SiteConfig
|
||||
if ($siteConfig -and $siteConfig.siteName -and $siteConfig.siteName -ne 'West Jefferson') {
|
||||
Write-PreInstallLog "Site config loaded - siteName: $($siteConfig.siteName)"
|
||||
foreach ($app in $config.Applications) {
|
||||
if ($app.InstallArgs -and $app.InstallArgs -match 'West Jefferson') {
|
||||
$app.InstallArgs = $app.InstallArgs -replace 'West Jefferson', $siteConfig.siteName
|
||||
Write-PreInstallLog " Overrode site name in $($app.Name) args: $($app.InstallArgs)"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog "No site-config override for siteName (using defaults in preinstall.json)"
|
||||
}
|
||||
|
||||
# --- Machine-number override: replace "9999" in UDC InstallArgs if tech entered a number ---
|
||||
$machineNumFile = 'C:\Enrollment\machine-number.txt'
|
||||
if (Test-Path -LiteralPath $machineNumFile) {
|
||||
$machineNum = (Get-Content -LiteralPath $machineNumFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
if ($machineNum -and $machineNum -ne '9999' -and $machineNum -match '^\d+$') {
|
||||
Write-PreInstallLog "Machine number from PXE menu: $machineNum"
|
||||
foreach ($app in $config.Applications) {
|
||||
if ($app.InstallArgs -and $app.InstallArgs -match '9999') {
|
||||
$app.InstallArgs = $app.InstallArgs -replace '9999', $machineNum
|
||||
Write-PreInstallLog " Overrode machine number in $($app.Name) args: $($app.InstallArgs)"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog "Machine number: $machineNum (default placeholder)"
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog "No machine-number.txt found (using 9999 default)"
|
||||
}
|
||||
|
||||
# --- Pre-stage UDC settings backup if available. UDC_Setup.exe tries to
|
||||
# pull udc_settings_<num>.json from \\tsgwp00525\shared\SPC\UDC\Settings_Backups\,
|
||||
# but at imaging time that share is unreachable (no SFLD creds yet).
|
||||
# We carry a local mirror of Settings_Backups on the PXE server at
|
||||
# Y:\pre-install\udc-backups\. Pre-creating C:\ProgramData\UDC\udc_settings.json
|
||||
# before UDC_Setup.exe runs means the installer's File.Copy (overwrite:true)
|
||||
# would overwrite it IF the share were reachable, but since it isn't, our
|
||||
# pre-staged file survives and UDC launches with correct settings.
|
||||
# UDC payload (settings backups + webserver settings) lives only in the
|
||||
# collections per-pc-type dir - UDC is the "C" of "collections". On nocoll
|
||||
# bays the dir doesn't exist; Test-Path skips silently.
|
||||
$udcCollDir = Join-Path (Split-Path $PSScriptRoot -Parent) 'gea-shopfloor-collections'
|
||||
if ($machineNum -and $machineNum -ne '9999') {
|
||||
$udcBackupDir = Join-Path $udcCollDir 'udc-backups'
|
||||
$udcBackup = Join-Path $udcBackupDir "udc_settings_$machineNum.json"
|
||||
$udcTarget = 'C:\ProgramData\UDC\udc_settings.json'
|
||||
if (Test-Path -LiteralPath $udcBackup) {
|
||||
if (-not (Test-Path 'C:\ProgramData\UDC')) {
|
||||
New-Item -Path 'C:\ProgramData\UDC' -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $udcBackup -Destination $udcTarget -Force
|
||||
Write-PreInstallLog "Pre-staged UDC settings from $udcBackup -> $udcTarget"
|
||||
} else {
|
||||
Write-PreInstallLog "No UDC settings backup for machine $machineNum at $udcBackup (skipping - normal for nocoll bays)"
|
||||
}
|
||||
}
|
||||
|
||||
$udcWebSrc = Join-Path $udcCollDir 'udc_webserver_settings.json'
|
||||
$udcWebDst = 'C:\ProgramData\UDC\udc_webserver_settings.json'
|
||||
if (Test-Path -LiteralPath $udcWebSrc) {
|
||||
if (-not (Test-Path 'C:\ProgramData\UDC')) {
|
||||
New-Item -Path 'C:\ProgramData\UDC' -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
Copy-Item -Path $udcWebSrc -Destination $udcWebDst -Force
|
||||
Write-PreInstallLog "Pre-staged UDC webserver settings from $udcWebSrc -> $udcWebDst"
|
||||
} else {
|
||||
Write-PreInstallLog "No UDC webserver settings file at $udcWebSrc (skipping - normal for nocoll bays)"
|
||||
}
|
||||
|
||||
# --- Suppress Windows Defender Firewall "Allow access" prompts globally for
|
||||
# this preinstall pass. Oracle's OUI extracts a JRE to a per-run
|
||||
# %TEMP%\OraInstall<timestamp>\jdk\jre\bin\java.exe and binds a localhost
|
||||
# port, which trips the unsigned-app firewall dialog. The temp path is
|
||||
# timestamp-randomized so a path-based -Program rule can't match it.
|
||||
# Turning off NotifyOnListen stops the prompt without weakening the
|
||||
# firewall's actual block decisions.
|
||||
try {
|
||||
Set-NetFirewallProfile -Profile Domain,Public,Private -NotifyOnListen False -ErrorAction Stop
|
||||
Write-PreInstallLog "Firewall NotifyOnListen disabled (Domain/Public/Private)"
|
||||
} catch {
|
||||
Write-PreInstallLog "Failed to disable firewall NotifyOnListen: $_" "WARN"
|
||||
}
|
||||
|
||||
# --- Pre-enable .NET Framework 3.5. Oracle 11.2 OUI uses a .NET 2.0/3.5
|
||||
# component on its welcome path; on Win10/11 NetFx3 is OFF by default
|
||||
# and the first 3.5 API call pops the "Download and install this feature"
|
||||
# dialog. Enable-WindowsOptionalFeature pulls payload from Windows Update
|
||||
# when available; on hosts without internet at preinstall time this will
|
||||
# fail and Oracle will still prompt (TODO: stage NetFx3 sxs cab on image
|
||||
# and pass -Source for true air-gap support). Idempotent no-op when
|
||||
# already enabled. Mirrors the per-PC enable in CMM/09-Setup-CMM.ps1.
|
||||
try {
|
||||
$netfx = Get-WindowsOptionalFeature -Online -FeatureName 'NetFx3' -ErrorAction Stop
|
||||
if ($netfx.State -eq 'Enabled') {
|
||||
Write-PreInstallLog ".NET Framework 3.5 already enabled"
|
||||
} else {
|
||||
Write-PreInstallLog ".NET Framework 3.5 state is $($netfx.State) - enabling now (may take a minute)..."
|
||||
$result = Enable-WindowsOptionalFeature -Online -FeatureName 'NetFx3' -All -NoRestart -ErrorAction Stop
|
||||
Write-PreInstallLog " Enable-WindowsOptionalFeature RestartNeeded=$($result.RestartNeeded)"
|
||||
}
|
||||
} catch {
|
||||
Write-PreInstallLog "Failed to enable .NET 3.5: $_" "WARN"
|
||||
Write-PreInstallLog " Continuing - Oracle and other 3.5-dependent installers may surface a Windows feature-install prompt"
|
||||
}
|
||||
|
||||
# --- Pre-create Windows Firewall rules for UDC + MTConnect Agent so the
|
||||
# installer doesn't pop firewall-allow dialogs during silent install.
|
||||
# Rules are idempotent (New-NetFirewallRule -ErrorAction SilentlyContinue
|
||||
# on existing rule name is a no-op in practice; we remove-then-add).
|
||||
$fwRules = @(
|
||||
@{ Name = 'UDC'; Program = 'C:\Program Files\UDC\UDC.exe' },
|
||||
@{ Name = 'UDC MTConnect Agent'; Program = 'C:\ProgramData\UDC\MTConnect_UDC\Agent\agent.exe' }
|
||||
)
|
||||
foreach ($r in $fwRules) {
|
||||
try {
|
||||
Remove-NetFirewallRule -DisplayName $r.Name -ErrorAction SilentlyContinue
|
||||
New-NetFirewallRule -DisplayName $r.Name -Direction Inbound -Program $r.Program `
|
||||
-Action Allow -Profile Any -ErrorAction Stop | Out-Null
|
||||
Write-PreInstallLog "Firewall rule created: $($r.Name) -> $($r.Program)"
|
||||
} catch {
|
||||
Write-PreInstallLog "Firewall rule '$($r.Name)' failed: $_" "WARN"
|
||||
}
|
||||
}
|
||||
|
||||
# --- Detection helper (mirrors Simple-Install.ps1's Test-ApplicationInstalled) ---
|
||||
function Test-AppInstalled {
|
||||
param($App)
|
||||
|
||||
if (-not $App.DetectionMethod) {
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($App.DetectionMethod) {
|
||||
"Registry" {
|
||||
if (-not (Test-Path $App.DetectionPath)) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionName) {
|
||||
$value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue
|
||||
if (-not $value) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionValue) {
|
||||
return ($value.$($App.DetectionName) -eq $App.DetectionValue)
|
||||
}
|
||||
return $true
|
||||
}
|
||||
return $true
|
||||
}
|
||||
"File" {
|
||||
return Test-Path $App.DetectionPath
|
||||
}
|
||||
default {
|
||||
Write-PreInstallLog " Unknown detection method: $($App.DetectionMethod)" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-PreInstallLog " Detection check threw: $_" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- Iterate apps ---
|
||||
$installed = 0
|
||||
$skipped = 0
|
||||
$failed = 0
|
||||
|
||||
foreach ($app in $config.Applications) {
|
||||
# Cancel any reboot a previous installer scheduled (some respect /norestart, some
|
||||
# don't - VC++ 2008's bootstrapper sometimes triggers an immediate Windows reboot
|
||||
# despite the flag). Doing this BEFORE each install protects the rest of the loop.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
Write-PreInstallLog "==> $($app.Name)"
|
||||
|
||||
# Filter by PCTypes - matches on wildcard, base type, composite type-subtype
|
||||
# key, OR any alias-equivalent name. The 2026-05-04 rename reorg gave us
|
||||
# gea-shopfloor-* PC type strings that need to match legacy ["Standard"],
|
||||
# ["CMM"], etc filters in preinstall.json (and vice versa: a future
|
||||
# preinstall.json that uses gea-shopfloor-* should still match a fleet PC
|
||||
# whose pc-type.txt is still "Standard").
|
||||
$allowedTypes = @($app.PCTypes)
|
||||
$aliasGroups = @(
|
||||
@('Standard', 'Standard-Machine','Standard-Timeclock','gea-shopfloor-collections','gea-shopfloor-nocollections','gea-shopfloor-common'),
|
||||
@('Standard-Machine', 'gea-shopfloor-collections','gea-shopfloor-nocollections'),
|
||||
@('Standard-Timeclock', 'gea-shopfloor-common'),
|
||||
@('CMM', 'gea-shopfloor-cmm'),
|
||||
@('Keyence', 'gea-shopfloor-keyence'),
|
||||
@('Lab', 'gea-shopfloor-common'),
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat'),
|
||||
@('PartMarker', 'gea-shopfloor-partmarker')
|
||||
)
|
||||
$myNames = New-Object System.Collections.Generic.HashSet[string]([System.StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($n in @($pcType, $pcProfileKey) | Where-Object { $_ }) {
|
||||
[void]$myNames.Add($n)
|
||||
foreach ($g in $aliasGroups) {
|
||||
if ($g -icontains $n) { foreach ($x in $g) { [void]$myNames.Add($x) } }
|
||||
}
|
||||
}
|
||||
# PCTypesStrict=true bypasses the alias-expansion matcher and requires
|
||||
# the actual pcType (or composite pcProfileKey) to literally equal one
|
||||
# of the allowedTypes entries. Used by UDC because the alias graph
|
||||
# transitively connects gea-shopfloor-collections <-> nocollections via
|
||||
# the legacy 'Standard' group, which would otherwise cause UDC to install
|
||||
# on nocoll bays even with PCTypes=['gea-shopfloor-collections'].
|
||||
$matchesType = ($allowedTypes -contains '*')
|
||||
if (-not $matchesType) {
|
||||
if ($app.PCTypesStrict) {
|
||||
foreach ($t in $allowedTypes) {
|
||||
if (($pcType -ieq $t) -or ($pcProfileKey -ieq $t)) { $matchesType = $true; break }
|
||||
}
|
||||
} else {
|
||||
foreach ($t in $allowedTypes) {
|
||||
if ($myNames.Contains($t)) { $matchesType = $true; break }
|
||||
foreach ($g in $aliasGroups) {
|
||||
if ($g -icontains $t) {
|
||||
foreach ($x in $g) { if ($myNames.Contains($x)) { $matchesType = $true; break } }
|
||||
}
|
||||
if ($matchesType) { break }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (-not $matchesType) {
|
||||
Write-PreInstallLog " PCTypes filter excludes '$pcProfileKey' (allowed: $($allowedTypes -join ', ')) - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Detection check
|
||||
if (Test-AppInstalled -App $app) {
|
||||
Write-PreInstallLog " Already installed - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Locate installer file
|
||||
$installerPath = Join-Path $installerDir $app.Installer
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
Write-PreInstallLog " Installer file not found: $installerPath" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
Write-PreInstallLog " Installing from $installerPath"
|
||||
if ($app.InstallArgs) {
|
||||
Write-PreInstallLog " InstallArgs: $($app.InstallArgs)"
|
||||
}
|
||||
|
||||
# Per-app verbose msiexec log path - written by /L*v on MSI installs and tailed
|
||||
# into the runner log on failure so we don't need to dig through C:\Logs manually.
|
||||
$safeName = $app.Name -replace '[^a-zA-Z0-9]','_'
|
||||
$msiLog = Join-Path $logDir "msi-$safeName.log"
|
||||
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
try {
|
||||
# We use [System.Diagnostics.Process]::Start() directly instead of Start-Process
|
||||
# because PowerShell 5.1's Start-Process -PassThru has a bug where the returned
|
||||
# Process object's OS handle is disposed when control returns to the script,
|
||||
# causing ExitCode to always read as $null even after WaitForExit() succeeds.
|
||||
# Calling Process.Start() directly returns a Process object with a live handle
|
||||
# that exposes ExitCode correctly. We poll HasExited so UDC_Setup.exe (which
|
||||
# hangs forever after spawning a hidden WPF window) can be killed via the
|
||||
# detection-during-install path.
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
||||
|
||||
if ($app.Type -eq "MSI") {
|
||||
$psi.FileName = "msiexec.exe"
|
||||
$psi.Arguments = "/i `"$installerPath`""
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments += " " + $app.InstallArgs
|
||||
}
|
||||
$psi.Arguments += " /L*v `"$msiLog`""
|
||||
Write-PreInstallLog " msiexec verbose log: $msiLog"
|
||||
}
|
||||
elseif ($app.Type -eq "EXE") {
|
||||
$psi.FileName = $installerPath
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments = $app.InstallArgs
|
||||
}
|
||||
# If the JSON entry specifies a LogFile (per-installer log written by
|
||||
# the EXE itself - e.g. Setup-OpenText.ps1 -> C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), surface it on failure the same way we surface the
|
||||
# msiexec /L*v verbose log for MSI installs. Lets EXE wrappers actually
|
||||
# report what went wrong inside.
|
||||
if ($app.LogFile) {
|
||||
Write-PreInstallLog " Installer log: $($app.LogFile)"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-PreInstallLog " Unsupported Type: $($app.Type) - skipping" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
|
||||
# Poll for completion: process exit OR detection success (whichever happens first)
|
||||
$timeoutSec = 600 # 10 min hard cap per app
|
||||
$pollInterval = 5
|
||||
$elapsed = 0
|
||||
$killedAfterDetect = $false
|
||||
|
||||
while ($elapsed -lt $timeoutSec) {
|
||||
if ($proc.HasExited) { break }
|
||||
|
||||
# Detection-during-install kill is OPT-IN via "KillAfterDetection: true"
|
||||
# in the JSON entry. It's only safe for installers that hang forever after
|
||||
# creating their detection markers (UDC_Setup.exe spawns a hidden WPF window
|
||||
# and never exits). For normal installers (Oracle, msiexec, etc.) the
|
||||
# detection registry key often gets written midway through the install AND
|
||||
# msiexec is still doing post-install cleanup AND msiserver is still holding
|
||||
# the install mutex - killing msiexec at that point leaves the system in a
|
||||
# bad state and the NEXT msiexec call returns 1618 ERROR_INSTALL_ALREADY_
|
||||
# RUNNING. So opt-out by default; only kill apps that explicitly need it.
|
||||
if ($app.KillAfterDetection -and (Test-AppInstalled -App $app)) {
|
||||
Write-PreInstallLog " Detection passed at $elapsed s - killing installer to advance (KillAfterDetection)"
|
||||
try { $proc.Kill(); $proc.WaitForExit(5000) | Out-Null } catch { }
|
||||
|
||||
# UDC's installer auto-launches UDC.exe silently; it's UDC.exe
|
||||
# (not UDC_Setup) that actually writes udc_settings.json from
|
||||
# the CLI args. Killing it immediately after the detection
|
||||
# marker appears loses the MachineNumber write. Wait (up to
|
||||
# 15s) for settings.json to reflect the target number, then
|
||||
# kill. Works uniformly for tech-typed numbers and the 9999
|
||||
# placeholder - whichever was passed as arg 2 is what we
|
||||
# expect to see persist.
|
||||
if ($app.Name -eq "UDC") {
|
||||
$udcJson = 'C:\ProgramData\UDC\udc_settings.json'
|
||||
$targetNum = $null
|
||||
if ($app.InstallArgs -match '-(\d+)\s*$') { $targetNum = $matches[1] }
|
||||
$waitSec = 0
|
||||
while ($waitSec -lt 15) {
|
||||
if (Test-Path $udcJson) {
|
||||
try {
|
||||
$mn = (Get-Content $udcJson -Raw -ErrorAction Stop |
|
||||
ConvertFrom-Json).GeneralSettings.MachineNumber
|
||||
if ($mn -and $mn -eq $targetNum) {
|
||||
Write-PreInstallLog " UDC persisted MachineNumber=$mn after $waitSec s"
|
||||
break
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
Start-Sleep -Seconds 1
|
||||
$waitSec++
|
||||
}
|
||||
if ($waitSec -ge 15) {
|
||||
Write-PreInstallLog " UDC did not persist MachineNumber within 15 s - killing anyway" "WARN"
|
||||
}
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill(); $_.WaitForExit(2000) | Out-Null } catch { }
|
||||
}
|
||||
}
|
||||
|
||||
$killedAfterDetect = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $pollInterval
|
||||
$elapsed += $pollInterval
|
||||
}
|
||||
|
||||
if (-not $proc.HasExited -and -not $killedAfterDetect) {
|
||||
Write-PreInstallLog " TIMEOUT after $timeoutSec seconds - killing installer" "ERROR"
|
||||
try { $proc.Kill() } catch { }
|
||||
if ($app.Name -eq "UDC") {
|
||||
Get-Process UDC -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
try { $_.Kill() } catch { }
|
||||
}
|
||||
}
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
if ($killedAfterDetect) {
|
||||
Write-PreInstallLog " SUCCESS (verified via detection during install)"
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
# Start-Process -PassThru returns a Process object whose ExitCode is null
|
||||
# until WaitForExit() is called - even if HasExited is already true. Without
|
||||
# this, every install that goes through the exit-code branch gets reported
|
||||
# as "Exit code - FAILED" (empty) regardless of the real result.
|
||||
try { $proc.WaitForExit() } catch { }
|
||||
$exitCode = $proc.ExitCode
|
||||
|
||||
if ($exitCode -eq 0 -or $exitCode -eq 3010) {
|
||||
Write-PreInstallLog " Exit code $exitCode after $elapsed s - SUCCESS"
|
||||
if ($exitCode -eq 3010) {
|
||||
Write-PreInstallLog " (Reboot required for $($app.Name))"
|
||||
}
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
Write-PreInstallLog " Exit code $exitCode - FAILED" "ERROR"
|
||||
|
||||
# If the JSON entry specifies a LogFile for an EXE install (e.g.
|
||||
# Setup-OpenText.ps1 writes its own log at C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), tail it here so the runner log surfaces the actual
|
||||
# cause without us having to dig through C:\Logs manually.
|
||||
if ($app.Type -eq "EXE" -and $app.LogFile -and (Test-Path $app.LogFile)) {
|
||||
Write-PreInstallLog " --- last 30 lines of $($app.LogFile) ---"
|
||||
Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-PreInstallLog " $_"
|
||||
}
|
||||
Write-PreInstallLog " --- end installer log tail ---"
|
||||
}
|
||||
|
||||
# Surface the *meaningful* lines from the verbose msiexec log (not just
|
||||
# the tail, which is rollback/cleanup noise). The actual root-cause lines
|
||||
# are: MSI error notes (Note: 1: <code>), failed action returns (return
|
||||
# value 3), errors logged by custom actions, and any line containing the
|
||||
# word "Error". Grep these out so the runner log shows the smoking gun.
|
||||
if ($app.Type -eq "MSI" -and (Test-Path $msiLog)) {
|
||||
Write-PreInstallLog " --- meaningful lines from $msiLog ---"
|
||||
$patterns = @(
|
||||
'Note: 1: ',
|
||||
'return value 3',
|
||||
'Error \d+\.',
|
||||
'CustomAction .* returned actual error',
|
||||
'Failed to ',
|
||||
'Installation failed',
|
||||
'Calling custom action',
|
||||
'1: 2262', # Database not found
|
||||
'1: 2203', # Cannot open database
|
||||
'1: 2330' # Insufficient permissions
|
||||
)
|
||||
$regex = ($patterns -join '|')
|
||||
$matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 30
|
||||
if ($matches) {
|
||||
foreach ($m in $matches) {
|
||||
Write-PreInstallLog " $($m.Line.Trim())"
|
||||
}
|
||||
} else {
|
||||
Write-PreInstallLog " (no error patterns matched - falling back to last 25 lines)"
|
||||
Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-PreInstallLog " $_"
|
||||
}
|
||||
}
|
||||
Write-PreInstallLog " --- end MSI log scan ---"
|
||||
}
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-PreInstallLog " Install threw: $_" "ERROR"
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-PreInstallLog "============================================"
|
||||
Write-PreInstallLog "PreInstall complete: $installed installed, $skipped skipped, $failed failed"
|
||||
Write-PreInstallLog "============================================"
|
||||
|
||||
# Final reboot cancel - if the last installer in the loop scheduled one, the
|
||||
# dispatcher's later `shutdown /a` won't fire until the next baseline script starts.
|
||||
# Cancel here so control returns cleanly to Run-ShopfloorSetup.ps1.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
exit 0
|
||||
@@ -1,127 +0,0 @@
|
||||
# 03-ShellDefaults.ps1 - Shell appearance defaults for all new user profiles.
|
||||
#
|
||||
# Applies the following tweaks so shopfloor PCs look/behave closer to the
|
||||
# Win10 baseline and don't reach out to Bing from the Start menu:
|
||||
# 1. Taskbar aligned to the left (TaskbarAl=0). Per-user setting, written
|
||||
# into the Default User hive so every NEW profile inherits it.
|
||||
# 2. Start menu "Recommended" section hidden (HideRecommendedSection=1).
|
||||
# 3. Web results + Bing search-box suggestions killed in Start/Search.
|
||||
# 4. Cortana disabled via policy (no-op on LTSC 2024 since Cortana isn't
|
||||
# shipped, but set anyway for belt-and-suspenders).
|
||||
#
|
||||
# 2-4 are HKLM policy values - W11 LTSC / Enterprise honour them; Home/Pro
|
||||
# do not. Fine here since the fleet is LTSC 2024.
|
||||
#
|
||||
# Existing profiles (SupportUser, pre-existing end-user accounts) are NOT
|
||||
# modified for TaskbarAl - the Default User hive is a template copied on
|
||||
# profile creation, not a live cascade. The HKLM policies affect everyone.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host "ERROR: 03-ShellDefaults.ps1 must run as Administrator." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# ---- 1. HKLM policy DWORDs (all LTSC-honoured) -------------------------------
|
||||
# Each entry: KeyPath, ValueName, ValueData. Key is created if missing.
|
||||
$hklmPolicies = @(
|
||||
# Hide Start menu "Recommended" section.
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'
|
||||
Name = 'HideRecommendedSection'; Value = 1 }
|
||||
|
||||
# Kill web results in Start/taskbar search.
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'
|
||||
Name = 'DisableWebSearch'; Value = 1 }
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'
|
||||
Name = 'ConnectedSearchUseWeb'; Value = 0 }
|
||||
|
||||
# Disable Cortana (policy; LTSC doesn't ship Cortana but this is harmless).
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'
|
||||
Name = 'AllowCortana'; Value = 0 }
|
||||
|
||||
# Kill Bing-powered search-box suggestions (per-keystroke telemetry).
|
||||
@{ Key = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'
|
||||
Name = 'DisableSearchBoxSuggestions'; Value = 1 }
|
||||
)
|
||||
|
||||
foreach ($p in $hklmPolicies) {
|
||||
try {
|
||||
if (-not (Test-Path $p.Key)) {
|
||||
New-Item -Path $p.Key -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $p.Key -Name $p.Name -Value $p.Value -Type DWord -Force
|
||||
Write-Host ("Set {0}\{1} = {2}" -f $p.Key, $p.Name, $p.Value)
|
||||
} catch {
|
||||
Write-Warning ("Failed to set {0}\{1}: {2}" -f $p.Key, $p.Name, $_)
|
||||
}
|
||||
}
|
||||
|
||||
# ---- 2. Set Adobe Acrobat Reader as default PDF viewer ----
|
||||
# OEMDefaultAssociations.xml is read when a new user profile is created.
|
||||
# Deploying it before ShopFloor's first logon means the profile lands with
|
||||
# Acrobat as the PDF handler instead of Edge. Existing profiles (SupportUser)
|
||||
# are not affected. DISM /import-defaultappassociations writes the XML to
|
||||
# C:\Windows\System32\OEMDefaultAssociations.xml and sets the policy key.
|
||||
$assocXml = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DefaultAssociations>
|
||||
<Association Identifier=".pdf" ProgId="Acrobat.Document.DC" ApplicationName="Adobe Acrobat" />
|
||||
</DefaultAssociations>
|
||||
"@
|
||||
$assocPath = Join-Path $env:TEMP 'DefaultAssociations.xml'
|
||||
try {
|
||||
Set-Content -Path $assocPath -Value $assocXml -Encoding UTF8 -Force
|
||||
$out = & dism.exe /online /import-defaultappassociations:"$assocPath" 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-Host "Set .pdf default to Acrobat.Document.DC via DISM"
|
||||
} else {
|
||||
Write-Warning "DISM import-defaultappassociations returned $LASTEXITCODE"
|
||||
}
|
||||
Remove-Item $assocPath -Force -ErrorAction SilentlyContinue
|
||||
} catch {
|
||||
Write-Warning "Failed to set default PDF viewer: $_"
|
||||
}
|
||||
|
||||
# ---- 3. Taskbar left-align via Default User hive ----
|
||||
$defaultHive = 'C:\Users\Default\NTUSER.DAT'
|
||||
$mountPoint = 'HKU\SFLDDefault'
|
||||
$regPath = 'Registry::HKEY_USERS\SFLDDefault\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $defaultHive)) {
|
||||
Write-Warning "Default User hive not found at $defaultHive - skipping taskbar tweak."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Unload a stale mount from a previous run, if any. reg unload returns
|
||||
# non-zero when nothing is mounted; ignore that.
|
||||
& reg.exe unload $mountPoint *> $null
|
||||
|
||||
$loadOutput = & reg.exe load $mountPoint $defaultHive 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "reg load failed ($LASTEXITCODE): $loadOutput"
|
||||
exit 1
|
||||
}
|
||||
|
||||
try {
|
||||
if (-not (Test-Path $regPath)) {
|
||||
New-Item -Path $regPath -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $regPath -Name 'TaskbarAl' -Value 0 -Type DWord -Force
|
||||
Write-Host "Set Default User TaskbarAl=0 (left-aligned)"
|
||||
} catch {
|
||||
Write-Warning "Failed to set TaskbarAl: $_"
|
||||
} finally {
|
||||
# Force GC so PowerShell drops its handles on the hive before unload,
|
||||
# otherwise reg unload fails with "The process cannot access the file".
|
||||
[gc]::Collect()
|
||||
[gc]::WaitForPendingFinalizers()
|
||||
[gc]::Collect()
|
||||
& reg.exe unload $mountPoint *> $null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Warning "reg unload returned $LASTEXITCODE - Default User hive may still be mounted"
|
||||
}
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,15 +0,0 @@
|
||||
# 04-NetworkAndWinRM.ps1 -- Set network profiles to Private and enable WinRM (baseline)
|
||||
|
||||
# --- Transcript ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
|
||||
try { Start-Transcript -Path (Join-Path $logDir '04-NetworkAndWinRM.log') -Append -Force | Out-Null } catch {}
|
||||
|
||||
# --- Set all network profiles to Private ---
|
||||
Get-NetConnectionProfile | Set-NetConnectionProfile -NetworkCategory Private
|
||||
Write-Host "All network profiles set to Private."
|
||||
|
||||
# --- Enable and configure WinRM ---
|
||||
Enable-PSRemoting -Force -SkipNetworkProfileCheck
|
||||
Write-Host "WinRM enabled."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
@@ -1,101 +0,0 @@
|
||||
# 05-OfficeShortcuts.ps1 - Public desktop + Default User Start Menu shortcuts for
|
||||
# the core Office apps (Excel, Word, PowerPoint).
|
||||
#
|
||||
# Only runs if Office is actually installed - self-detects via EXE existence so it
|
||||
# silently no-ops on PC types whose ppkg didn't include Office (Display kiosks,
|
||||
# Wax/Trace, etc.). Office Click-to-Run installs the binaries but doesn't create
|
||||
# desktop shortcuts; this script fills that gap.
|
||||
#
|
||||
# Shortcuts land in two places:
|
||||
# - C:\Users\Public\Desktop\ - visible to all users immediately
|
||||
# - C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\
|
||||
# - inherited by every NEW user profile
|
||||
# (Azure AD logons after the device is
|
||||
# enrolled)
|
||||
#
|
||||
# Idempotent: overwrites existing .lnk files each run.
|
||||
|
||||
# --- Transcript ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch {} }
|
||||
try { Start-Transcript -Path (Join-Path $logDir '05-OfficeShortcuts.log') -Append -Force | Out-Null } catch {}
|
||||
|
||||
$officeApps = @(
|
||||
@{ Exe = 'EXCEL.EXE'; Name = 'Excel' }
|
||||
@{ Exe = 'WINWORD.EXE'; Name = 'Word' }
|
||||
@{ Exe = 'POWERPNT.EXE'; Name = 'PowerPoint' }
|
||||
)
|
||||
|
||||
# Office binary location varies by install type and version:
|
||||
# - Office 2019+ / M365 / LTSC Click-to-Run: <PF>\Microsoft Office\root\Office16\
|
||||
# - Office 2016 MSI (legacy): <PF>\Microsoft Office\Office16\ (no root\)
|
||||
# - Office 2013 MSI: <PF>\Microsoft Office\Office15\
|
||||
# - x86 Office on 64-bit Windows: Program Files (x86)\...
|
||||
# The standard ShopFloor Office ppkg is x86 Click-to-Run so normal case
|
||||
# is "Program Files (x86)\Microsoft Office\root\Office16\EXCEL.EXE", but
|
||||
# we check all variants so the script works across SKUs.
|
||||
$officeSearchRoots = @(
|
||||
'C:\Program Files\Microsoft Office\root\Office16',
|
||||
'C:\Program Files (x86)\Microsoft Office\root\Office16',
|
||||
'C:\Program Files\Microsoft Office\Office16',
|
||||
'C:\Program Files (x86)\Microsoft Office\Office16',
|
||||
'C:\Program Files\Microsoft Office\Office15',
|
||||
'C:\Program Files (x86)\Microsoft Office\Office15'
|
||||
)
|
||||
|
||||
$officeRoot = $null
|
||||
foreach ($base in $officeSearchRoots) {
|
||||
if (Test-Path (Join-Path $base 'EXCEL.EXE')) {
|
||||
$officeRoot = $base
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $officeRoot) {
|
||||
Write-Host "No Office install detected - skipping shortcut creation."
|
||||
Write-Host "Searched:"
|
||||
foreach ($p in $officeSearchRoots) {
|
||||
$hit = if (Test-Path $p) { "(dir exists)" } else { "(missing)" }
|
||||
Write-Host " $p $hit"
|
||||
}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Office detected at: $officeRoot"
|
||||
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$defaultStart = 'C:\Users\Default\AppData\Roaming\Microsoft\Windows\Start Menu\Programs'
|
||||
|
||||
if (-not (Test-Path $defaultStart)) {
|
||||
New-Item -Path $defaultStart -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
|
||||
foreach ($app in $officeApps) {
|
||||
$exePath = Join-Path $officeRoot $app.Exe
|
||||
if (-not (Test-Path $exePath)) {
|
||||
Write-Warning "$($app.Name) EXE not found at $exePath - skipping."
|
||||
continue
|
||||
}
|
||||
foreach ($dest in @($publicDesktop, $defaultStart)) {
|
||||
$lnkPath = Join-Path $dest "$($app.Name).lnk"
|
||||
try {
|
||||
$sc = $shell.CreateShortcut($lnkPath)
|
||||
$sc.TargetPath = $exePath
|
||||
$sc.WorkingDirectory = $officeRoot
|
||||
$sc.IconLocation = "$exePath,0"
|
||||
$sc.Save()
|
||||
Write-Host " $($app.Name) -> $lnkPath"
|
||||
}
|
||||
catch {
|
||||
Write-Warning "Failed to create $lnkPath : $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null
|
||||
Write-Host "Office shortcuts created."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,634 +0,0 @@
|
||||
# 06-OrganizeDesktop.ps1 - Single source of truth for the Public Desktop
|
||||
# layout. Builds three category folders and fills them, so end users see
|
||||
# a clean desktop instead of 20+ loose icons.
|
||||
#
|
||||
# Creates three subfolders under C:\Users\Public\Desktop:
|
||||
# Office\ - Excel, Word, PowerPoint, Outlook, OneNote, etc.
|
||||
# Shopfloor Tools\ - UDC, eDNC, HostExplorer, MarkZebra, PC-DMIS, Hexagon,
|
||||
# WJ Shopfloor, Defect_Tracker, NTLARS, etc.
|
||||
# Web Links\ - .url shortcuts (internal portals)
|
||||
#
|
||||
# Two phases:
|
||||
# PHASE 1 (Invoke-DesktopSweep): walks everything at the desktop ROOT,
|
||||
# classifies each .lnk / .url by filename / extension / .lnk target, and
|
||||
# moves it into the right category folder. Unknown items are LEFT AT
|
||||
# THE ROOT on purpose. eDNC.lnk and NTLARS.lnk are allowlisted to stay
|
||||
# at the root - end users click them too often to bury in a folder.
|
||||
#
|
||||
# PHASE 2 (Add-ShopfloorToolsApps): materializes specific app shortcuts
|
||||
# into Shopfloor Tools\ from known .exe paths (UDC, eDNC, NTLARS). For
|
||||
# MSI-advertised shortcuts that don't have a resolvable target (WJ
|
||||
# Shopfloor, Defect_Tracker), finds the existing .lnk wherever it lives
|
||||
# and copies it in. This runs AFTER the sweep so it overwrites any
|
||||
# stale copies the sweeper may have moved in.
|
||||
#
|
||||
# Also registers an "Organize Public Desktop" scheduled task that re-runs
|
||||
# the sweep (phase 1) at every logon. Phase 2 doesn't re-run - the
|
||||
# shortcuts it creates are stable and 06 on first run populates them.
|
||||
#
|
||||
# 07-TaskbarLayout.ps1 reads the result of this script and writes
|
||||
# LayoutModification.xml to pin specific shortcuts to the taskbar.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# ============================================================================
|
||||
# Admin check - writing to C:\Users\Public\Desktop\* and registering
|
||||
# system-wide scheduled tasks both require elevation. Fail fast if not
|
||||
# admin so we don't spam half-successful garbage across the output.
|
||||
# ============================================================================
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: 06-OrganizeDesktop.ps1 must run as Administrator." -ForegroundColor Red
|
||||
Write-Host " Re-run from an elevated PowerShell:" -ForegroundColor Red
|
||||
Write-Host " Start -> type 'powershell' -> right-click -> Run as administrator" -ForegroundColor Red
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Transcript. 06 runs as a SYSTEM scheduled task at every logon via the
|
||||
# 'Organize Public Desktop' task, so without a transcript its decisions
|
||||
# (what got added, what got suppressed) are invisible. Append mode keeps
|
||||
# a history across logons; session header + PID makes individual runs
|
||||
# easy to find.
|
||||
$transcriptDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $transcriptDir)) {
|
||||
try { New-Item -ItemType Directory -Path $transcriptDir -Force | Out-Null } catch {}
|
||||
}
|
||||
$transcriptPath = Join-Path $transcriptDir '06-OrganizeDesktop.log'
|
||||
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host "=== 06-OrganizeDesktop session start (PID $PID, $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
Write-Host "================================================================"
|
||||
|
||||
# ============================================================================
|
||||
# Cleanup: remove 'Shopfloor Intune Sync' task after sync completes
|
||||
#
|
||||
# Monitor-IntuneProgress.ps1 registers that task as BUILTIN\Users + RunLevel
|
||||
# Limited (needs a user context to show the QR code / Configure-PC prompt),
|
||||
# and a Limited task cannot Unregister-ScheduledTask itself. So when sync
|
||||
# completes it just writes C:\Enrollment\sync-complete.txt as a marker and
|
||||
# the task stays around firing on every logon as a no-op.
|
||||
#
|
||||
# 06 runs as SYSTEM via its own sweep task, which DOES have permission to
|
||||
# delete any task. When we see the marker, drop the stale task so Task
|
||||
# Scheduler doesn't keep the dead entry forever.
|
||||
# ============================================================================
|
||||
$syncCompleteMarker = 'C:\Enrollment\sync-complete.txt'
|
||||
$syncTaskName = 'Shopfloor Intune Sync'
|
||||
if (Test-Path -LiteralPath $syncCompleteMarker) {
|
||||
$syncTask = Get-ScheduledTask -TaskName $syncTaskName -ErrorAction SilentlyContinue
|
||||
if ($syncTask) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $syncTaskName -Confirm:$false -ErrorAction Stop
|
||||
Write-Host "Removed stale '$syncTaskName' task (sync complete, marker present)."
|
||||
} catch {
|
||||
Write-Warning "Failed to remove '$syncTaskName' task: $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Load site config + PC profile
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools'
|
||||
$scriptPath = $MyInvocation.MyCommand.Path
|
||||
$scriptDir = Split-Path -Parent $scriptPath
|
||||
|
||||
# Filenames that always stay at the desktop root regardless of classification.
|
||||
# Empty right now - every shortcut lives inside a category folder for a
|
||||
# clean end-user desktop. Add entries here if a future shortcut needs to
|
||||
# stay pinned at root.
|
||||
$keepAtRoot = @()
|
||||
|
||||
# ============================================================================
|
||||
# Phase 1: Sweep loose shortcuts at desktop root into category folders
|
||||
# ============================================================================
|
||||
function Invoke-DesktopSweep {
|
||||
param([string]$DesktopPath)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $DesktopPath)) {
|
||||
Write-Host "Public desktop not found at $DesktopPath - skipping sweep."
|
||||
return
|
||||
}
|
||||
|
||||
# Category definitions. Order matters: first match wins. Each category
|
||||
# can classify by file Extension, file Name (regex), or .lnk Target
|
||||
# (regex, resolved via WScript.Shell).
|
||||
$categories = [ordered]@{
|
||||
'Office' = @{
|
||||
Name = @(
|
||||
'^(Excel|Word|PowerPoint|Outlook|OneNote|Access|Publisher|OneDrive)(\s|$)'
|
||||
)
|
||||
Target = @(
|
||||
'\\(EXCEL|WINWORD|POWERPNT|OUTLOOK|ONENOTE|MSACCESS|MSPUB)\.EXE$',
|
||||
'\\OneDrive\.exe$'
|
||||
)
|
||||
}
|
||||
'Shopfloor Tools' = @{
|
||||
Name = @(
|
||||
'^UDC',
|
||||
'eDNC', '\bDNC\b', 'DncMain', 'GE DNC', 'NTLARS',
|
||||
'Host\s*Explorer', 'TN3270', 'TN5250', 'HE\s*3270', 'HE\s*5250',
|
||||
# OpenText / 'WJ Shopfloor' / 'ShopFloor' shortcuts left on
|
||||
# the desktop intentionally. The actual filename varies by
|
||||
# OpenText profile (e.g. 'WJ Shopfloor OpenText.lnk') so the
|
||||
# taskbar pin path mismatch silently skipped these. Leaving
|
||||
# them at the public desktop top level instead.
|
||||
'Defect[_\s-]?Tracker',
|
||||
'MarkZebra', 'Zebra',
|
||||
'PC-?DMIS',
|
||||
'Hexagon', 'CLM\s*Tools',
|
||||
'Blancco',
|
||||
'Keyence'
|
||||
)
|
||||
Target = @(
|
||||
'\\UDC\.exe$',
|
||||
'DncMain\.exe$', 'NTLARS\.exe$', 'DNCRemote',
|
||||
'HostExplorer\.exe$', 'hostex32\.exe$', 'humicon15\.exe$',
|
||||
'PC-?DMIS',
|
||||
'Hexagon',
|
||||
'Defect_Tracker\.application$'
|
||||
)
|
||||
}
|
||||
'Web Links' = @{
|
||||
Extension = @('.url')
|
||||
}
|
||||
}
|
||||
|
||||
# Category folders are created LAZILY - only when we're about to move
|
||||
# something into them - so a Display / Wax-Trace PC without Office
|
||||
# doesn't get an empty Office\ folder littering the desktop. See
|
||||
# Remove-EmptyCategoryFolders at the end of the script for the
|
||||
# post-sweep cleanup pass that finishes the job.
|
||||
|
||||
# WScript.Shell for resolving .lnk targets
|
||||
$shell = $null
|
||||
try { $shell = New-Object -ComObject WScript.Shell } catch {}
|
||||
|
||||
# Only sweep files at the desktop root, never recurse into the category
|
||||
# folders themselves (that would loop things back).
|
||||
$items = @(Get-ChildItem -LiteralPath $DesktopPath -File -ErrorAction SilentlyContinue)
|
||||
|
||||
$moved = 0
|
||||
$skipped = 0
|
||||
|
||||
foreach ($item in $items) {
|
||||
# Allowlist check: never sweep items the user wants to keep visible
|
||||
if ($keepAtRoot -contains $item.Name) {
|
||||
Write-Host " keep: $($item.Name) (allowlisted)"
|
||||
continue
|
||||
}
|
||||
|
||||
$category = $null
|
||||
|
||||
# 1. Extension match (cheapest)
|
||||
foreach ($cat in $categories.Keys) {
|
||||
$exts = $categories[$cat].Extension
|
||||
if ($exts -and ($exts -contains $item.Extension)) {
|
||||
$category = $cat
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# 2. File name regex match
|
||||
if (-not $category) {
|
||||
foreach ($cat in $categories.Keys) {
|
||||
$pats = $categories[$cat].Name
|
||||
if (-not $pats) { continue }
|
||||
foreach ($p in $pats) {
|
||||
if ($item.BaseName -match $p) {
|
||||
$category = $cat
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($category) { break }
|
||||
}
|
||||
}
|
||||
|
||||
# 3. .lnk target resolution
|
||||
if (-not $category -and $item.Extension -ieq '.lnk' -and $shell) {
|
||||
try {
|
||||
$sc = $shell.CreateShortcut($item.FullName)
|
||||
$target = $sc.TargetPath
|
||||
if ($target) {
|
||||
foreach ($cat in $categories.Keys) {
|
||||
$pats = $categories[$cat].Target
|
||||
if (-not $pats) { continue }
|
||||
foreach ($p in $pats) {
|
||||
if ($target -match $p) {
|
||||
$category = $cat
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($category) { break }
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if ($category) {
|
||||
$destDir = Join-Path $DesktopPath $category
|
||||
if (-not (Test-Path -LiteralPath $destDir)) {
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $destDir -Force -ErrorAction Stop | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Failed to create category folder '$category' : $_"
|
||||
continue
|
||||
}
|
||||
}
|
||||
$destPath = Join-Path $destDir $item.Name
|
||||
try {
|
||||
Move-Item -LiteralPath $item.FullName -Destination $destPath -Force -ErrorAction Stop
|
||||
Write-Host " moved: $($item.Name) -> $category\"
|
||||
$moved++
|
||||
} catch {
|
||||
Write-Warning "Failed to move $($item.Name) : $_"
|
||||
}
|
||||
} else {
|
||||
$skipped++
|
||||
}
|
||||
}
|
||||
|
||||
if ($shell) {
|
||||
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null } catch {}
|
||||
}
|
||||
|
||||
Write-Host "Sweep complete: $moved moved, $skipped unclassified (left at root)."
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 2: Materialize specific app shortcuts into Shopfloor Tools\
|
||||
#
|
||||
# Runs AFTER the sweep. Creates fresh .lnk files for apps we can target by
|
||||
# .exe path (UDC, eDNC, NTLARS) and copies existing .lnk files for apps
|
||||
# that are MSI-advertised and have no resolvable target (WJ Shopfloor,
|
||||
# Defect_Tracker). Overwrites whatever the sweeper may have moved in -
|
||||
# this is intentional because fresh shortcuts are more reliable than
|
||||
# whatever install crud might be floating around.
|
||||
#
|
||||
# Each app is CONDITIONAL - if its source isn't present on this PC, the
|
||||
# entry is silently skipped. Safe to run on all shopfloor PC types.
|
||||
# ============================================================================
|
||||
|
||||
function New-ShopfloorLnk {
|
||||
param(
|
||||
[string]$Path,
|
||||
[string]$Target,
|
||||
[string]$Arguments = '',
|
||||
[string]$WorkingDirectory = ''
|
||||
)
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
try {
|
||||
$sc = $wsh.CreateShortcut($Path)
|
||||
$sc.TargetPath = $Target
|
||||
if ($Arguments) { $sc.Arguments = $Arguments }
|
||||
if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory }
|
||||
elseif ($Target) {
|
||||
$parent = Split-Path -Parent $Target
|
||||
if ($parent) { $sc.WorkingDirectory = $parent }
|
||||
}
|
||||
$sc.IconLocation = "$Target,0"
|
||||
$sc.Save()
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to create $Path : $_"
|
||||
return $false
|
||||
} finally {
|
||||
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
# Look up an existing .lnk file across the paths where installers / ppkgs
|
||||
# typically drop them. Used for apps where we don't build the .lnk from
|
||||
# an .exe (because the target is an MSI-advertised Darwin descriptor).
|
||||
function Find-ExistingLnk {
|
||||
param([string]$Filename)
|
||||
|
||||
$candidates = @(
|
||||
(Join-Path $publicDesktop $Filename),
|
||||
(Join-Path $shopfloorToolsDir $Filename),
|
||||
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\$Filename",
|
||||
"C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Shopfloor Tools\$Filename"
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path -LiteralPath $c) { return $c }
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Add-ShopfloorToolsApps {
|
||||
# App registry - each entry describes how to materialize one shortcut
|
||||
# into Shopfloor Tools\. Folder creation is deferred until we know we
|
||||
# have at least one app to put in it (lazy creation, so PC types with
|
||||
# nothing installed don't get an empty Shopfloor Tools folder).
|
||||
#
|
||||
# Kind = 'exe' -> build a fresh .lnk from ExePath
|
||||
# Kind = 'existing' -> copy an existing .lnk via Find-ExistingLnk
|
||||
$cfgApps = Get-ProfileValue 'desktopApps'
|
||||
|
||||
if ($null -ne $cfgApps -and $cfgApps.Count -gt 0) {
|
||||
$apps = @($cfgApps | ForEach-Object {
|
||||
$entry = @{ Name = $_.name; Kind = $_.kind }
|
||||
if ($_.kind -eq 'exe') { $entry.ExePath = $_.exePath }
|
||||
if ($_.kind -eq 'existing') { $entry.SourceName = $_.sourceName }
|
||||
$entry
|
||||
})
|
||||
} else {
|
||||
$apps = @(
|
||||
@{ Name = 'UDC'; Kind = 'exe'; ExePath = 'C:\Program Files\UDC\UDC.exe' }
|
||||
@{ Name = 'eDNC'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\bin\DncMain.exe' }
|
||||
@{ Name = 'NTLARS'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\Dnc\Common\NTLARS.exe' }
|
||||
@{ Name = 'WJ Shopfloor'; Kind = 'existing'; SourceName = 'WJ Shopfloor.lnk' }
|
||||
@{ Name = 'Defect_Tracker'; Kind = 'exe'; ExePath = 'C:\Program Files (x86)\WJF_Defect_Tracker\Defect_Tracker.exe' }
|
||||
)
|
||||
}
|
||||
|
||||
# Lazy folder creation - only create Shopfloor Tools\ the first time
|
||||
# we have an app that's actually going to land in it. PC types with
|
||||
# nothing installed get no empty folder.
|
||||
$ensureToolsDir = {
|
||||
if (-not (Test-Path -LiteralPath $shopfloorToolsDir)) {
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $shopfloorToolsDir -Force -ErrorAction Stop | Out-Null
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to create $shopfloorToolsDir : $_"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
return $true
|
||||
}
|
||||
|
||||
foreach ($app in $apps) {
|
||||
$dest = Join-Path $shopfloorToolsDir "$($app.Name).lnk"
|
||||
|
||||
switch ($app.Kind) {
|
||||
'exe' {
|
||||
if (Test-Path -LiteralPath $app.ExePath) {
|
||||
if (-not (& $ensureToolsDir)) { break }
|
||||
if (New-ShopfloorLnk -Path $dest -Target $app.ExePath) {
|
||||
Write-Host " created: $($app.Name) -> $dest"
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($app.Name) - target not installed ($($app.ExePath))" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
'existing' {
|
||||
$src = Find-ExistingLnk $app.SourceName
|
||||
if ($src) {
|
||||
# Skip if the sweep already moved the source into the
|
||||
# destination folder (src == dest -> "cannot overwrite
|
||||
# the item with itself").
|
||||
if ((Resolve-Path -LiteralPath $src -ErrorAction SilentlyContinue).Path -eq
|
||||
(Join-Path $shopfloorToolsDir $app.SourceName)) {
|
||||
Write-Host " exists: $($app.Name) (already in Shopfloor Tools)"
|
||||
} else {
|
||||
if (-not (& $ensureToolsDir)) { break }
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $dest -Force -ErrorAction Stop
|
||||
Write-Host " copied: $($app.Name) from $src"
|
||||
} catch {
|
||||
Write-Warning "Failed to copy $src -> $dest : $_"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($app.Name) - no source .lnk found" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 3: Remove empty category folders
|
||||
#
|
||||
# If the sweep classified nothing into a given category (e.g. no Office
|
||||
# on a Display PC, no Shopfloor Tools apps on a kiosk), we don't want an
|
||||
# empty folder cluttering the desktop. The scheduled-task sweep also runs
|
||||
# this so categories that go empty between logons self-heal.
|
||||
# ============================================================================
|
||||
function Remove-EmptyCategoryFolders {
|
||||
foreach ($cat in @('Office', 'Shopfloor Tools', 'Web Links')) {
|
||||
$dir = Join-Path $publicDesktop $cat
|
||||
if (-not (Test-Path -LiteralPath $dir)) { continue }
|
||||
$contents = @(Get-ChildItem -LiteralPath $dir -Force -ErrorAction SilentlyContinue)
|
||||
if ($contents.Count -eq 0) {
|
||||
try {
|
||||
Remove-Item -LiteralPath $dir -Force -ErrorAction Stop
|
||||
Write-Host " removed empty: $cat\"
|
||||
} catch {
|
||||
Write-Warning "Failed to remove empty $dir : $_"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Phase 4: Edge default browser + startup tabs (delegated to 08)
|
||||
#
|
||||
# 08-EdgeDefaultBrowser.ps1 resolves startup-tab URLs from .url files on
|
||||
# the Public Desktop (or in Web Links\ after the sweep). The .url files
|
||||
# are delivered by DSC AFTER this initial shopfloor setup, so the first
|
||||
# run at imaging time won't find them (falls back to hardcoded URLs,
|
||||
# Plant Apps gets skipped). By calling 08 from inside 06, every SYSTEM
|
||||
# scheduled-task logon re-run of 06 also re-runs 08 -- so after DSC drops
|
||||
# the .url files and the next sweep files them into Web Links\, 08
|
||||
# picks them up and updates the Edge policy. Self-healing.
|
||||
# ============================================================================
|
||||
function Invoke-EdgeDefaultBrowser {
|
||||
$edgeScript = Join-Path $scriptDir '08-EdgeDefaultBrowser.ps1'
|
||||
if (-not (Test-Path -LiteralPath $edgeScript)) {
|
||||
Write-Host " 08-EdgeDefaultBrowser.ps1 not found - skipping"
|
||||
return
|
||||
}
|
||||
try {
|
||||
& $edgeScript
|
||||
} catch {
|
||||
Write-Warning "08-EdgeDefaultBrowser.ps1 failed: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Scheduled task registration (re-runs 06 at every logon as SYSTEM)
|
||||
# ============================================================================
|
||||
function Register-SweepScheduledTask {
|
||||
param([string]$ScriptPath)
|
||||
|
||||
$taskName = 'Organize Public Desktop'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ScriptPath)) {
|
||||
Write-Warning "Cannot register scheduled task - script not found at $ScriptPath"
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$ScriptPath`""
|
||||
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
|
||||
# Run as SYSTEM so the script can (a) pass its admin check at the
|
||||
# top, (b) write to Public Desktop / ProgramData Start Menu / the
|
||||
# Default User profile without Access Denied, and (c) register the
|
||||
# next iteration of itself idempotently. Earlier versions used
|
||||
# -GroupId 'BUILTIN\Users' -RunLevel Limited which fired at logon
|
||||
# but the script exited immediately on the non-admin check.
|
||||
$principal = New-ScheduledTaskPrincipal `
|
||||
-UserId 'NT AUTHORITY\SYSTEM' `
|
||||
-LogonType ServiceAccount `
|
||||
-RunLevel Highest
|
||||
|
||||
$settings = New-ScheduledTaskSettingsSet `
|
||||
-AllowStartIfOnBatteries `
|
||||
-DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable `
|
||||
-ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
|
||||
# -ErrorAction Stop is required because Register-ScheduledTask emits
|
||||
# non-terminating errors on failure (Access Denied etc), which try/catch
|
||||
# would otherwise miss and we'd falsely claim success.
|
||||
Register-ScheduledTask `
|
||||
-TaskName $taskName `
|
||||
-Action $action `
|
||||
-Trigger $trigger `
|
||||
-Principal $principal `
|
||||
-Settings $settings `
|
||||
-Force `
|
||||
-ErrorAction Stop | Out-Null
|
||||
|
||||
Write-Host "Scheduled task '$taskName' registered (runs at every logon)."
|
||||
} catch {
|
||||
Write-Warning "Failed to register scheduled task : $_"
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Main
|
||||
# ============================================================================
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 0: create Office shortcuts (if Office is installed) ==="
|
||||
# Delegated to 05-OfficeShortcuts.ps1. Office is installed via ppkg but
|
||||
# doesn't finish streaming until AFTER the first reboot, so the first
|
||||
# imaging-time run of 06 finds no Office and this no-ops. On the second
|
||||
# logon (after reboot), Office is installed and 05 creates the shortcuts
|
||||
# so Phase 1 below can sweep them into the Office\ folder.
|
||||
$officeScript = Join-Path $scriptDir '05-OfficeShortcuts.ps1'
|
||||
if (Test-Path -LiteralPath $officeScript) {
|
||||
try { & $officeScript } catch { Write-Warning "05-OfficeShortcuts.ps1 failed: $_" }
|
||||
} else {
|
||||
Write-Host " 05-OfficeShortcuts.ps1 not found - skipping"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 1: sweep loose shortcuts into category folders ==="
|
||||
Invoke-DesktopSweep -DesktopPath $publicDesktop
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 2: populate Shopfloor Tools with app shortcuts ==="
|
||||
Add-ShopfloorToolsApps
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 3: auto-apply startup items from PC profile ==="
|
||||
# Creates .lnk files in AllUsers Startup folder based on the profile's
|
||||
# startupItems. This runs as SYSTEM at every logon via the sweep scheduled
|
||||
# task, so any item the tech removes via Configure-PC would bounce right
|
||||
# back unless we remember the removal. startup-overrides.json tracks labels
|
||||
# the tech has explicitly suppressed; items in that list are skipped here
|
||||
# and re-enabling them via Configure-PC removes them from the list.
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
|
||||
$overridesPath = 'C:\ProgramData\GE\Shopfloor\startup-overrides.json'
|
||||
|
||||
$suppressed = @()
|
||||
if (Test-Path -LiteralPath $overridesPath) {
|
||||
try {
|
||||
$overrides = Get-Content -LiteralPath $overridesPath -Raw | ConvertFrom-Json
|
||||
if ($overrides.suppressed) { $suppressed = @($overrides.suppressed) }
|
||||
} catch {
|
||||
Write-Warning " Failed to parse $overridesPath : $_"
|
||||
}
|
||||
}
|
||||
|
||||
$cfgStartup = Get-ProfileValue 'startupItems'
|
||||
|
||||
if ($null -ne $cfgStartup -and $cfgStartup.Count -gt 0) {
|
||||
if (-not (Test-Path $startupDir)) {
|
||||
New-Item -ItemType Directory -Path $startupDir -Force | Out-Null
|
||||
}
|
||||
$edgePath = @(
|
||||
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
||||
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
foreach ($si in $cfgStartup) {
|
||||
if ($suppressed -contains $si.label) {
|
||||
Write-Host " suppressed: $($si.label) (tech disabled via Configure-PC)"
|
||||
continue
|
||||
}
|
||||
$lnkPath = Join-Path $startupDir "$($si.label).lnk"
|
||||
# Skip if already exists (idempotent - don't overwrite user changes)
|
||||
if (Test-Path -LiteralPath $lnkPath) {
|
||||
Write-Host " exists: $($si.label)"
|
||||
continue
|
||||
}
|
||||
switch ($si.type) {
|
||||
'exe' {
|
||||
if (Test-Path -LiteralPath $si.target) {
|
||||
if (New-ShopfloorLnk -Path $lnkPath -Target $si.target) {
|
||||
Write-Host " added: $($si.label)"
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($si.label) - target not installed" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
'existing' {
|
||||
$src = Find-ExistingLnk $si.sourceLnk
|
||||
if ($src) {
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $lnkPath -Force
|
||||
Write-Host " added: $($si.label)"
|
||||
} catch { Write-Warning " failed: $($si.label) - $_" }
|
||||
} else {
|
||||
Write-Host " skip: $($si.label) - source not found" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
'url' {
|
||||
$url = $null
|
||||
if ($si.urlKey -and $siteConfig -and $siteConfig.urls) {
|
||||
$url = $siteConfig.urls.$($si.urlKey)
|
||||
}
|
||||
if ($url -and $edgePath) {
|
||||
if (New-ShopfloorLnk -Path $lnkPath -Target $edgePath -Arguments "--new-window `"$url`"") {
|
||||
Write-Host " added: $($si.label)"
|
||||
}
|
||||
} else {
|
||||
Write-Host " skip: $($si.label) - URL or Edge not available" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host " (no startup items in profile)"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 4: remove empty category folders ==="
|
||||
Remove-EmptyCategoryFolders
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 5: Edge default browser + startup tabs ==="
|
||||
Invoke-EdgeDefaultBrowser
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== Phase 6: register logon sweeper scheduled task ==="
|
||||
Register-SweepScheduledTask -ScriptPath $scriptPath
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "=== 06-OrganizeDesktop session end ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')) ==="
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,156 +0,0 @@
|
||||
# 07-TaskbarLayout.ps1 - Minimal taskbar pinner.
|
||||
#
|
||||
# Reads the shortcuts that 06-OrganizeDesktop.ps1 created in
|
||||
# C:\Users\Public\Desktop\Shopfloor Tools\ and writes a LayoutModification.xml
|
||||
# that pins them to the taskbar along with Microsoft Edge.
|
||||
#
|
||||
# This script does NOT create, move, or copy any shortcuts - all shortcut
|
||||
# management lives in 06. 07 is the last-mile taskbar config only.
|
||||
#
|
||||
# Pin order (left to right): Edge, WJ Shopfloor, UDC, eDNC, NTLARS, Defect_Tracker.
|
||||
#
|
||||
# LayoutModification.xml is written to the Default User profile shell
|
||||
# directory, which means the pins apply on FIRST LOGON of any new user
|
||||
# profile. Existing profiles don't re-read the Default User template, so
|
||||
# they won't pick up the pins without a manual re-pin or profile delete.
|
||||
# This is a Windows design limitation - syspin.exe style hacks are the
|
||||
# only workaround for existing profiles, and they're unsupported.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# ============================================================================
|
||||
# Admin check - writing to the Default User profile requires elevation.
|
||||
# ============================================================================
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: 07-TaskbarLayout.ps1 must run as Administrator." -ForegroundColor Red
|
||||
Write-Host " Re-run from an elevated PowerShell." -ForegroundColor Red
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Load site config + PC profile
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$shopfloorToolsDir = Join-Path $publicDesktop 'Shopfloor Tools'
|
||||
$defaultUserShell = 'C:\Users\Default\AppData\Local\Microsoft\Windows\Shell'
|
||||
$layoutXmlPath = Join-Path $defaultUserShell 'LayoutModification.xml'
|
||||
|
||||
# ============================================================================
|
||||
# Pin list: exact ordered list of shortcut names to pin. Each entry points
|
||||
# at a .lnk file via a Windows environment-variable-expanded path. Edge is
|
||||
# special-cased to its default Start Menu location since Windows maintains
|
||||
# it; all other entries reference C:\Users\Public\Desktop\Shopfloor Tools\
|
||||
# which 06-OrganizeDesktop.ps1 populates.
|
||||
# ============================================================================
|
||||
$cfgPins = Get-ProfileValue 'taskbarPins'
|
||||
|
||||
if ($null -ne $cfgPins -and $cfgPins.Count -gt 0) {
|
||||
$pinSpec = @($cfgPins | ForEach-Object {
|
||||
@{
|
||||
Name = $_.name
|
||||
Path = $_.lnkPath
|
||||
Literal = [Environment]::ExpandEnvironmentVariables($_.lnkPath)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
$pinSpec = @(
|
||||
@{
|
||||
Name = 'Microsoft Edge'
|
||||
Path = '%ALLUSERSPROFILE%\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk'
|
||||
Literal = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Microsoft Edge.lnk'
|
||||
}
|
||||
@{
|
||||
Name = 'WJ Shopfloor'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\WJ Shopfloor.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'WJ Shopfloor.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'UDC'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\UDC.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'UDC.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'eDNC'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\eDNC.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'eDNC.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'NTLARS'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\NTLARS.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'NTLARS.lnk')
|
||||
}
|
||||
@{
|
||||
Name = 'Defect_Tracker'
|
||||
Path = '%PUBLIC%\Desktop\Shopfloor Tools\Defect_Tracker.lnk'
|
||||
Literal = (Join-Path $shopfloorToolsDir 'Defect_Tracker.lnk')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Build the pin list - skip any whose .lnk is missing
|
||||
# ============================================================================
|
||||
Write-Host ""
|
||||
Write-Host "Checking which Shopfloor Tools shortcuts exist..."
|
||||
|
||||
$pinPaths = @()
|
||||
foreach ($pin in $pinSpec) {
|
||||
if (Test-Path -LiteralPath $pin.Literal) {
|
||||
Write-Host " pin: $($pin.Name) -> $($pin.Literal)"
|
||||
$pinPaths += $pin.Path
|
||||
} else {
|
||||
Write-Host " skip: $($pin.Name) - not found at $($pin.Literal)" -ForegroundColor DarkGray
|
||||
}
|
||||
}
|
||||
|
||||
if ($pinPaths.Count -eq 0) {
|
||||
Write-Warning "No pins to apply (nothing found in Shopfloor Tools\). Did 06-OrganizeDesktop.ps1 run first?"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Write LayoutModification.xml
|
||||
# ============================================================================
|
||||
$pinXml = ($pinPaths | ForEach-Object {
|
||||
" <taskbar:DesktopApp DesktopApplicationLinkPath=`"$_`"/>"
|
||||
}) -join "`r`n"
|
||||
|
||||
$layoutXml = @"
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LayoutModificationTemplate
|
||||
xmlns="http://schemas.microsoft.com/Start/2014/LayoutModification"
|
||||
xmlns:defaultlayout="http://schemas.microsoft.com/Start/2014/FullDefaultLayout"
|
||||
xmlns:start="http://schemas.microsoft.com/Start/2014/StartLayout"
|
||||
xmlns:taskbar="http://schemas.microsoft.com/Start/2014/TaskbarLayout"
|
||||
Version="1">
|
||||
<CustomTaskbarLayoutCollection PinListPlacement="Replace">
|
||||
<defaultlayout:TaskbarLayout>
|
||||
<taskbar:TaskbarPinList>
|
||||
$pinXml
|
||||
</taskbar:TaskbarPinList>
|
||||
</defaultlayout:TaskbarLayout>
|
||||
</CustomTaskbarLayoutCollection>
|
||||
</LayoutModificationTemplate>
|
||||
"@
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $defaultUserShell)) {
|
||||
New-Item -ItemType Directory -Path $defaultUserShell -Force -ErrorAction Stop | Out-Null
|
||||
}
|
||||
# -ErrorAction Stop so Access Denied becomes a catchable terminating error
|
||||
# instead of silently falling through to the success message.
|
||||
Set-Content -LiteralPath $layoutXmlPath -Value $layoutXml -Encoding UTF8 -Force -ErrorAction Stop
|
||||
Write-Host ""
|
||||
Write-Host "Wrote taskbar layout with $($pinPaths.Count) pin(s) to:"
|
||||
Write-Host " $layoutXmlPath"
|
||||
Write-Host ""
|
||||
Write-Host "Pins will apply on first logon of any NEW user profile."
|
||||
} catch {
|
||||
Write-Warning "Failed to write $layoutXmlPath : $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,319 +0,0 @@
|
||||
# 08-EdgeDefaultBrowser.ps1 - Set Microsoft Edge as the default browser
|
||||
# and PDF handler for all new user profiles, and configure startup tabs
|
||||
# so Edge opens with Plant Apps / Shop Floor Homepage / Shopfloor Dashboard
|
||||
# when the end user first launches it.
|
||||
#
|
||||
# WHY THIS EXISTS:
|
||||
# The West Jefferson shopfloor ppkg installs Google Chrome alongside Edge.
|
||||
# On first URL click in a new Azure AD user profile, Windows pops a "Choose
|
||||
# your default app" picker because multiple browsers are installed and
|
||||
# nothing has been marked as default. End users hit this on every fresh PC
|
||||
# and it derails the shopfloor workflow.
|
||||
#
|
||||
# HOW IT WORKS:
|
||||
# Two layers, belt-and-suspenders:
|
||||
#
|
||||
# 1. DISM default-app-associations XML
|
||||
# Writes an XML file mapping http/https/.htm/.html/.pdf/.svg/etc to
|
||||
# Edge's ProgIds, then runs:
|
||||
# dism /Online /Import-DefaultAppAssociations:<xml>
|
||||
# This imports the XML into the Default User profile template. Any
|
||||
# NEW user profile created after this point inherits the associations
|
||||
# on first logon. Microsoft-supported, works across Win10/Win11.
|
||||
#
|
||||
# 2. Group Policy registry key
|
||||
# HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\DefaultAssociationsConfiguration
|
||||
# points at the same XML file. This is the "Set a default associations
|
||||
# configuration file" GPO. With this set, Windows re-applies the XML
|
||||
# on every logon, not just once at profile creation - so Windows
|
||||
# update defaults-reset and similar edge cases don't un-do us.
|
||||
#
|
||||
# CAVEATS:
|
||||
# - Applies to NEW profiles on first logon. An existing profile that's
|
||||
# already been logged in (e.g. SupportUser) won't pick up the change
|
||||
# without a manual Settings > Default apps visit. Acceptable because
|
||||
# SupportUser isn't the end-user account.
|
||||
# - Does not UNinstall Chrome. Chrome remains available, it's just not
|
||||
# the default handler for URLs.
|
||||
# - Microsoft blocks programmatic UserChoice hash tampering since Win10
|
||||
# 1703. The DISM + policy route is the only supported path, and it
|
||||
# only fires for new profiles - that's by design.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Admin check
|
||||
# ----------------------------------------------------------------------------
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host ""
|
||||
Write-Host "ERROR: 08-EdgeDefaultBrowser.ps1 must run as Administrator." -ForegroundColor Red
|
||||
Write-Host " Re-run from an elevated PowerShell." -ForegroundColor Red
|
||||
Write-Host ""
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Load site config + PC profile
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Sanity: Edge must be installed for this to mean anything
|
||||
# ----------------------------------------------------------------------------
|
||||
$edgeCandidates = @(
|
||||
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
||||
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
||||
)
|
||||
$edgePath = $edgeCandidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
if (-not $edgePath) {
|
||||
Write-Host "Microsoft Edge not found in either Program Files location - skipping default browser config."
|
||||
exit 0
|
||||
}
|
||||
Write-Host "Found Edge at: $edgePath"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# XML: Edge as default for web browsing + PDFs + common image formats
|
||||
# ----------------------------------------------------------------------------
|
||||
$xml = @'
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<DefaultAssociations>
|
||||
<Association Identifier=".htm" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".html" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".mht" ProgId="MSEdgeMHT" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".mhtml" ProgId="MSEdgeMHT" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".pdf" ProgId="MSEdgePDF" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".svg" ProgId="MSEdgeSVG" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier=".webp" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="http" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="https" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="ftp" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
<Association Identifier="microsoft-edge" ProgId="MSEdgeHTM" ApplicationName="Microsoft Edge" />
|
||||
</DefaultAssociations>
|
||||
'@
|
||||
|
||||
# Write to a stable location that persists across reimaging (C:\Enrollment is
|
||||
# the canonical staging dir in this repo, survives logoff/logon, referenced
|
||||
# by the GP policy below so it must be a path every user-session can read).
|
||||
$xmlDir = 'C:\Enrollment'
|
||||
$xmlPath = Join-Path $xmlDir 'edge-default-associations.xml'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $xmlDir)) {
|
||||
New-Item -ItemType Directory -Path $xmlDir -Force | Out-Null
|
||||
}
|
||||
Set-Content -LiteralPath $xmlPath -Value $xml -Encoding UTF8 -Force
|
||||
Write-Host "Wrote associations XML to: $xmlPath"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Layer 1: DISM import into the Default User profile template
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "Layer 1: dism /Online /Import-DefaultAppAssociations..."
|
||||
& dism.exe /Online /Import-DefaultAppAssociations:"$xmlPath" | Out-Null
|
||||
$dismExit = $LASTEXITCODE
|
||||
if ($dismExit -eq 0) {
|
||||
Write-Host " OK - defaults imported into Default User template."
|
||||
} else {
|
||||
Write-Warning " dism returned exit code $dismExit - import may not have applied."
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Layer 2: Group Policy registry key - enforces XML on every logon
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "Layer 2: Setting DefaultAssociationsConfiguration policy..."
|
||||
$polKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\System'
|
||||
if (-not (Test-Path $polKey)) {
|
||||
New-Item -Path $polKey -Force | Out-Null
|
||||
}
|
||||
Set-ItemProperty -Path $polKey -Name 'DefaultAssociationsConfiguration' -Value $xmlPath -Type String -Force
|
||||
Write-Host " OK - HKLM:\SOFTWARE\Policies\Microsoft\Windows\System\DefaultAssociationsConfiguration = $xmlPath"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host "Edge default browser config applied."
|
||||
Write-Host "================================================================"
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Edge startup tabs + homepage
|
||||
#
|
||||
# End users should see Plant Apps, the Shop Floor Homepage, and the
|
||||
# Shopfloor Dashboard open in three tabs the moment Edge launches. Rather
|
||||
# than hardcoding URLs in this script, we read them out of the .url files
|
||||
# that already live on the Public Desktop (copied there by ppkg). This
|
||||
# way, if an admin renames or retargets a .url file later, the script
|
||||
# picks up the new URL on the next imaging run without a code change.
|
||||
#
|
||||
# Fallback: if the .url file is missing, we fall back to a hardcoded URL
|
||||
# (only for the ones we know for sure). Plant Apps has no fallback because
|
||||
# I don't have its URL memorized.
|
||||
#
|
||||
# Machine policies (all under HKLM:\SOFTWARE\Policies\Microsoft\Edge):
|
||||
# RestoreOnStartup = 4 (open a specific set of URLs)
|
||||
# RestoreOnStartupURLs = subkey with "1","2","3"... = URLs in tab order
|
||||
# HomepageLocation = first startup URL (home button opens same)
|
||||
# HomepageIsNewTabPage = 0 (home button opens HomepageLocation, not NTP)
|
||||
# ShowHomeButton = 1 (show the home button in the toolbar)
|
||||
# ----------------------------------------------------------------------------
|
||||
Write-Host ""
|
||||
Write-Host "Configuring Edge startup tabs..."
|
||||
|
||||
function Get-UrlFromFile {
|
||||
param([string]$Path)
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $Path -ErrorAction Stop
|
||||
$urlLine = $content | Where-Object { $_ -match '^URL=' } | Select-Object -First 1
|
||||
if ($urlLine) {
|
||||
return ($urlLine -replace '^URL=', '').Trim()
|
||||
}
|
||||
} catch {}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Resolve-StartupUrl {
|
||||
param(
|
||||
[string]$BaseName, # .url filename without extension
|
||||
[string]$Fallback = ''
|
||||
)
|
||||
# Look in Public Desktop root first, then Web Links subfolder (06's
|
||||
# sweeper may have moved it there already).
|
||||
$candidates = @(
|
||||
"C:\Users\Public\Desktop\$BaseName.url",
|
||||
"C:\Users\Public\Desktop\Web Links\$BaseName.url"
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (Test-Path -LiteralPath $c) {
|
||||
$url = Get-UrlFromFile $c
|
||||
if ($url) {
|
||||
Write-Host " resolved $BaseName -> $url (from $c)"
|
||||
return $url
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($Fallback) {
|
||||
Write-Host " fallback $BaseName -> $Fallback (.url file not found)" -ForegroundColor DarkGray
|
||||
return $Fallback
|
||||
}
|
||||
Write-Warning " $BaseName : no .url file found and no fallback - will be skipped"
|
||||
return $null
|
||||
}
|
||||
|
||||
# Resolve startup tabs: profile > site-wide > hardcoded fallback
|
||||
# Different PC types can have different tabs (e.g. Lab has Webmail
|
||||
# instead of Plant Apps, Standard-Machine has Plant Apps).
|
||||
$cfgTabs = Get-ProfileValue 'edgeStartupTabs'
|
||||
|
||||
$startupTabs = @()
|
||||
|
||||
if ($null -ne $cfgTabs -and $cfgTabs.Count -gt 0) {
|
||||
foreach ($tab in $cfgTabs) {
|
||||
$fallback = if ($tab.fallbackUrlKey -and $siteConfig.urls) { $siteConfig.urls.$($tab.fallbackUrlKey) } else { '' }
|
||||
$url = Resolve-StartupUrl -BaseName $tab.baseName -Fallback $fallback
|
||||
if ($url) { $startupTabs += $url }
|
||||
}
|
||||
} else {
|
||||
$plantApps = Resolve-StartupUrl -BaseName 'Plant Apps' -Fallback 'https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications'
|
||||
if ($plantApps) { $startupTabs += $plantApps }
|
||||
|
||||
$shopFloorHome = Resolve-StartupUrl -BaseName 'WJ Shop Floor Homepage' -Fallback 'https://tsgwp00525.wjs.geaerospace.net'
|
||||
if ($shopFloorHome) { $startupTabs += $shopFloorHome }
|
||||
|
||||
$dashboard = Resolve-StartupUrl -BaseName 'Shopfloor Dashboard' -Fallback 'https://tsgwp00525.wjs.geaerospace.net/shopdb/shopfloor-dashboard/'
|
||||
if ($dashboard) { $startupTabs += $dashboard }
|
||||
}
|
||||
|
||||
# Resolve homepage: profile can override (e.g. Lab has a different homepage
|
||||
# than Standard). Falls back to the first startup tab if not specified.
|
||||
$edgeHomepage = if ($pcProfile -and $pcProfile.edgeHomepage) { $pcProfile.edgeHomepage } else { $null }
|
||||
|
||||
if ($startupTabs.Count -eq 0) {
|
||||
Write-Warning "No startup tab URLs resolved - skipping Edge startup config."
|
||||
} else {
|
||||
$edgePolKey = 'HKLM:\SOFTWARE\Policies\Microsoft\Edge'
|
||||
if (-not (Test-Path $edgePolKey)) {
|
||||
New-Item -Path $edgePolKey -Force | Out-Null
|
||||
}
|
||||
|
||||
# RestoreOnStartup = 4 means "open a specific set of URLs" (the list
|
||||
# lives under the RestoreOnStartupURLs subkey below).
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'RestoreOnStartup' -Value 4 -Type DWord -Force
|
||||
|
||||
# Build RestoreOnStartupURLs subkey: numbered string values "1","2","3"
|
||||
$urlsKey = Join-Path $edgePolKey 'RestoreOnStartupURLs'
|
||||
if (Test-Path $urlsKey) {
|
||||
# Wipe any prior entries so we don't leak stale tabs if the list
|
||||
# shrinks between runs
|
||||
Remove-Item -Path $urlsKey -Recurse -Force
|
||||
}
|
||||
New-Item -Path $urlsKey -Force | Out-Null
|
||||
for ($i = 0; $i -lt $startupTabs.Count; $i++) {
|
||||
$name = [string]($i + 1)
|
||||
Set-ItemProperty -Path $urlsKey -Name $name -Value $startupTabs[$i] -Type String -Force
|
||||
}
|
||||
|
||||
# Homepage: profile override > first startup tab. Home button opens it, not NTP.
|
||||
$homepageUrl = if ($edgeHomepage) { $edgeHomepage } else { $startupTabs[0] }
|
||||
Write-Host "Homepage set to: $homepageUrl"
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'HomepageLocation' -Value $homepageUrl -Type String -Force
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'HomepageIsNewTabPage' -Value 0 -Type DWord -Force
|
||||
Set-ItemProperty -Path $edgePolKey -Name 'ShowHomeButton' -Value 1 -Type DWord -Force
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Edge startup tabs set ($($startupTabs.Count) tab(s)):"
|
||||
for ($i = 0; $i -lt $startupTabs.Count; $i++) {
|
||||
Write-Host " $($i + 1). $($startupTabs[$i])"
|
||||
}
|
||||
Write-Host "Homepage set to: $($startupTabs[0])"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Chrome: same homepage + startup tabs as Edge
|
||||
#
|
||||
# The PPKG installs Chrome alongside Edge. Apply the same profile-driven
|
||||
# homepage and startup tabs so both browsers behave identically. Uses the
|
||||
# same $startupTabs and $homepageUrl resolved above.
|
||||
# HKLM:\SOFTWARE\Policies\Google\Chrome mirrors the Edge policy structure.
|
||||
# ============================================================================
|
||||
Write-Host ""
|
||||
Write-Host "Configuring Chrome startup tabs..."
|
||||
|
||||
$chromePath = @(
|
||||
'C:\Program Files\Google\Chrome\Application\chrome.exe',
|
||||
'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe'
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $chromePath) {
|
||||
Write-Host " Chrome not installed - skipping." -ForegroundColor DarkGray
|
||||
} elseif ($startupTabs.Count -eq 0) {
|
||||
Write-Host " No startup tabs resolved - skipping Chrome config." -ForegroundColor DarkGray
|
||||
} else {
|
||||
$chromePolKey = 'HKLM:\SOFTWARE\Policies\Google\Chrome'
|
||||
if (-not (Test-Path $chromePolKey)) {
|
||||
New-Item -Path $chromePolKey -Force | Out-Null
|
||||
}
|
||||
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'RestoreOnStartup' -Value 4 -Type DWord -Force
|
||||
|
||||
$urlsKey = Join-Path $chromePolKey 'RestoreOnStartupURLs'
|
||||
if (Test-Path $urlsKey) {
|
||||
Remove-Item -Path $urlsKey -Recurse -Force
|
||||
}
|
||||
New-Item -Path $urlsKey -Force | Out-Null
|
||||
for ($i = 0; $i -lt $startupTabs.Count; $i++) {
|
||||
Set-ItemProperty -Path $urlsKey -Name ([string]($i + 1)) -Value $startupTabs[$i] -Type String -Force
|
||||
}
|
||||
|
||||
$chromeHomepage = if ($homepageUrl) { $homepageUrl } elseif ($edgeHomepage) { $edgeHomepage } else { $startupTabs[0] }
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'HomepageLocation' -Value $chromeHomepage -Type String -Force
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'HomepageIsNewTabPage' -Value 0 -Type DWord -Force
|
||||
Set-ItemProperty -Path $chromePolKey -Name 'ShowHomeButton' -Value 1 -Type DWord -Force
|
||||
|
||||
Write-Host " Chrome startup tabs set ($($startupTabs.Count) tab(s)), homepage: $chromeHomepage"
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "================================================================"
|
||||
Write-Host "08-EdgeDefaultBrowser.ps1 complete."
|
||||
Write-Host "Edge + Chrome homepage and startup tabs configured."
|
||||
Write-Host "Machine-wide GPO - applies to all users on next browser launch."
|
||||
Write-Host "================================================================"
|
||||
|
||||
exit 0
|
||||
@@ -1,89 +0,0 @@
|
||||
# 09-Install-PrinterInstallerMap.ps1 - imaging-time placement of the
|
||||
# PrinterInstallerMap.exe tool plus a Public Desktop shortcut.
|
||||
# Runs for every shopfloor PC type (Standard, CMM, Keyence, Display,
|
||||
# etc.) so a tech can launch the printer installer at any bay without
|
||||
# going through Edge. Numbered 09 to run after 06-OrganizeDesktop
|
||||
# (which creates Shopfloor Tools folder) and 08-EdgeDefaultBrowser.
|
||||
#
|
||||
# Why this exists: the legacy printer-deploy flow was a web map
|
||||
# (printerinstallermap.asp) that handed users a per-selection .bat to
|
||||
# download and run. New Edge policy blocks both .bat downloads and
|
||||
# unsigned .exe runs. PrinterInstallerMap is a forked installer with the
|
||||
# map UX baked into a custom Inno wizard page; baking the EXE onto the
|
||||
# image at PXE time means it never goes through a browser (no MOTW, no
|
||||
# SmartScreen prompt). UAC still fires because PrivilegesRequired=admin
|
||||
# and supportuser creds satisfy it - that is the only remaining gate.
|
||||
#
|
||||
# Layout this script produces on a Standard PC:
|
||||
# C:\Tools\PrinterInstallerMap\PrinterInstallerMap.exe
|
||||
# C:\Users\Public\Desktop\Shopfloor Tools\Printer Installer.lnk
|
||||
#
|
||||
# The Shopfloor Tools desktop folder is also created/maintained by
|
||||
# 06-OrganizeDesktop.ps1; this script tolerates either order (it creates
|
||||
# the folder if absent so it can run before 06).
|
||||
#
|
||||
# Idempotent: re-imaging or re-running the script overwrites the EXE and
|
||||
# rewrites the shortcut. Drop-in newer PrinterInstallerMap.exe alongside
|
||||
# this script in the imaging payload, re-image the PC, the desktop link
|
||||
# now points to the new build.
|
||||
#
|
||||
# Source EXE expected at $PSScriptRoot\PrinterInstallerMap.exe (placed
|
||||
# next to this script when the imaging payload lands). Until the Inno
|
||||
# compile + sign+ship step is wired in (see ../../docs/ + the inno repo
|
||||
# feature/printer-map branch), this script no-ops with a warning if the
|
||||
# EXE is missing - re-image will pick it up once the build artifact is
|
||||
# dropped in.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$src = Join-Path $PSScriptRoot 'PrinterInstallerMap.exe'
|
||||
$installDir = 'C:\Tools\PrinterInstallerMap'
|
||||
$installExe = Join-Path $installDir 'PrinterInstallerMap.exe'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$shopfloorTools = Join-Path $publicDesktop 'Shopfloor Tools'
|
||||
$shortcutPath = Join-Path $shopfloorTools 'Printer Installer.lnk'
|
||||
|
||||
if (-not (Test-Path -LiteralPath $src)) {
|
||||
Write-Warning "Install-PrinterInstallerMap: source EXE not found at $src - skipping. Re-image after the build artifact is dropped alongside this script."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Stage EXE
|
||||
if (-not (Test-Path -LiteralPath $installDir)) {
|
||||
New-Item -Path $installDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $installExe -Force -ErrorAction Stop
|
||||
Write-Host "Install-PrinterInstallerMap: staged EXE -> $installExe"
|
||||
} catch {
|
||||
Write-Error "Install-PrinterInstallerMap: failed to copy EXE: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Ensure Shopfloor Tools folder exists. 06-OrganizeDesktop.ps1 also
|
||||
# creates this; we create it here so this script does not depend on
|
||||
# 06 having run first.
|
||||
if (-not (Test-Path -LiteralPath $shopfloorTools)) {
|
||||
try {
|
||||
New-Item -Path $shopfloorTools -ItemType Directory -Force | Out-Null
|
||||
} catch {
|
||||
Write-Warning "Install-PrinterInstallerMap: could not create $shopfloorTools : $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Create / overwrite shortcut
|
||||
try {
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
$lnk = $wsh.CreateShortcut($shortcutPath)
|
||||
$lnk.TargetPath = $installExe
|
||||
$lnk.IconLocation = "$installExe,0"
|
||||
$lnk.WorkingDirectory = $installDir
|
||||
$lnk.Description = 'Install network printers from the WJ site map'
|
||||
$lnk.Save()
|
||||
Write-Host "Install-PrinterInstallerMap: shortcut -> $shortcutPath"
|
||||
} catch {
|
||||
Write-Error "Install-PrinterInstallerMap: failed to create shortcut: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,115 +0,0 @@
|
||||
# Apply-MachineNumber.ps1 - SYSTEM-context worker for the two-task machine
|
||||
# number flow. Triggered on-demand by Prompt-MachineNumber.ps1 via
|
||||
# `schtasks /run /tn "WT-Apply-MachineNumber"`. Reads the requested number
|
||||
# from a file the GUI script wrote, invokes Update-MachineNumber as SYSTEM
|
||||
# (full HKLM + ProgramData access), writes a result JSON for the GUI to
|
||||
# display, then cleans up.
|
||||
#
|
||||
# Why SYSTEM:
|
||||
# The eDNC reg key (HKLM:\SOFTWARE\WOW6432Node\GE Aircraft Engines\DNC\
|
||||
# General\MachineNo) and UDC settings JSON live in HKLM + ProgramData
|
||||
# respectively - both require admin to write. The OLD design granted
|
||||
# BUILTIN\Users SetValue + Modify via 02-MachineNumberACLs.ps1, but that
|
||||
# was fragile (timing race with eDNC install, ACL silently failed on
|
||||
# some bays) AND a security hole (any user could mess with the machine
|
||||
# identity). Two-task design: GUI gathers input as logged-in user, SYSTEM
|
||||
# does the actual write.
|
||||
#
|
||||
# Files:
|
||||
# C:\Logs\SFLD\machine-number-request.txt - input, single line, new number
|
||||
# C:\Logs\SFLD\machine-number-result.json - output, status fields for GUI
|
||||
# C:\Logs\SFLD\Apply-MachineNumber.log - transcript
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path -LiteralPath $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcript = Join-Path $logDir 'Apply-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {}
|
||||
|
||||
$requestFile = Join-Path $logDir 'machine-number-request.txt'
|
||||
$resultFile = Join-Path $logDir 'machine-number-result.json'
|
||||
|
||||
function Write-Result {
|
||||
param([hashtable]$Body)
|
||||
$Body | ConvertTo-Json -Depth 5 | Set-Content -LiteralPath $resultFile -Encoding ascii -Force
|
||||
}
|
||||
|
||||
Write-Host "Apply-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
try {
|
||||
if (-not (Test-Path -LiteralPath $requestFile)) {
|
||||
Write-Warning "No request file at $requestFile - nothing to apply."
|
||||
Write-Result @{ Status = 'NoRequest'; Errors = @("request file missing: $requestFile") }
|
||||
exit 0
|
||||
}
|
||||
$newNumber = (Get-Content -LiteralPath $requestFile -First 1 -ErrorAction Stop).Trim()
|
||||
Write-Host "Requested new machine number: $newNumber"
|
||||
|
||||
if ($newNumber -notmatch '^\d+$') {
|
||||
Write-Warning "Request is not digits-only: '$newNumber'"
|
||||
Write-Result @{ Status = 'BadInput'; Requested = $newNumber; Errors = @("Not digits only: '$newNumber'") }
|
||||
Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Dot-source the shared helper. Update-MachineNumber.ps1 now has
|
||||
# -ErrorAction Stop on the writes so failures actually throw.
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
$mnResult = Update-MachineNumber -NewNumber $newNumber -Site $site
|
||||
|
||||
$resultBody = @{
|
||||
Status = if ($mnResult.Errors.Count -eq 0) { 'OK' } else { 'PartialErrors' }
|
||||
Requested = $newNumber
|
||||
Site = $site
|
||||
UdcUpdated = [bool]$mnResult.UdcUpdated
|
||||
EdncUpdated = [bool]$mnResult.EdncUpdated
|
||||
OldUdc = $mnResult.OldUdc
|
||||
OldEdnc = $mnResult.OldEdnc
|
||||
UdcSettingsRestored = [bool]$mnResult.UdcSettingsRestored
|
||||
UdcRestored = [bool]$mnResult.UdcRestored
|
||||
MTConnectUpdated = $mnResult.MTConnectUpdated
|
||||
MachineNumberTxtUpdated = [bool]$mnResult.MachineNumberTxtUpdated
|
||||
Errors = $mnResult.Errors
|
||||
AppliedAt = (Get-Date -Format 'o')
|
||||
AppliedAs = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||
}
|
||||
Write-Result -Body $resultBody
|
||||
|
||||
Write-Host "Update-MachineNumber result:"
|
||||
Write-Host " UdcUpdated = $($mnResult.UdcUpdated)"
|
||||
Write-Host " EdncUpdated = $($mnResult.EdncUpdated)"
|
||||
Write-Host " Errors = $($mnResult.Errors.Count)"
|
||||
if ($mnResult.Errors) { $mnResult.Errors | ForEach-Object { Write-Host " FAILED: $_" } }
|
||||
|
||||
Remove-Item -LiteralPath $requestFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# On clean success, also unregister the Prompt logon task. Prompt itself
|
||||
# tries to self-unregister but it runs as a Limited user (BUILTIN\Users)
|
||||
# and silently fails on Unregister-ScheduledTask (no delete right on a
|
||||
# SYSTEM-registered task). We're running as SYSTEM here, so we can.
|
||||
# Idempotent if Prompt already unregistered itself somehow.
|
||||
if ($mnResult.Errors.Count -eq 0 -and $mnResult.EdncUpdated) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName 'Prompt Machine Number' -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName 'Prompt Machine Number' -Confirm:$false -ErrorAction Stop
|
||||
Write-Host "Unregistered 'Prompt Machine Number' task (SYSTEM cleanup)."
|
||||
}
|
||||
} catch {
|
||||
Write-Host "Could not unregister 'Prompt Machine Number': $_"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Apply-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
} catch {
|
||||
Write-Warning "Apply threw: $_"
|
||||
Write-Result @{ Status = 'Exception'; Errors = @("$_") }
|
||||
} finally {
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
# Check-MachineNumber.ps1 - Logon-triggered check for placeholder machine
|
||||
# number. If UDC or eDNC are still at 9999, pops an InputBox for the user
|
||||
# to enter the real number. On success, unregisters the scheduled task so
|
||||
# the prompt never appears again.
|
||||
#
|
||||
# Runs as the LOGGED-IN USER (not SYSTEM) because it needs to show GUI.
|
||||
# Writing to ProgramData + HKLM is possible because 02-MachineNumberACLs.ps1
|
||||
# pre-granted BUILTIN\Users write access on those specific targets during
|
||||
# imaging.
|
||||
#
|
||||
# Registered/unregistered by Configure-PC.ps1 (item 6 in the toggle list).
|
||||
|
||||
# --- Transcript logging ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcriptPath = Join-Path $logDir 'Check-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
||||
Write-Host "Check-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
$taskName = 'Check Machine Number'
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
|
||||
# --- Read current values ---
|
||||
$currentMN = Get-CurrentMachineNumber
|
||||
$currentUdc = $currentMN.Udc
|
||||
$currentEdnc = $currentMN.Ednc
|
||||
|
||||
# --- Check if placeholder ---
|
||||
Write-Host "UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not found)' })"
|
||||
Write-Host "eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not found)' })"
|
||||
|
||||
if ($currentUdc -ne '9999' -and $currentEdnc -ne '9999') {
|
||||
Write-Host "Machine number is set (not 9999). Unregistering task and exiting."
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Placeholder 9999 detected - showing prompt."
|
||||
|
||||
# --- Show prompt ---
|
||||
$promptLines = @()
|
||||
$promptLines += "The machine number on this PC is still set to the"
|
||||
$promptLines += "placeholder value (9999). Please enter the correct"
|
||||
$promptLines += "machine number for this workstation."
|
||||
$promptLines += ""
|
||||
if ($currentUdc) { $promptLines += "Current UDC: $currentUdc" }
|
||||
if ($currentEdnc) { $promptLines += "Current eDNC: $currentEdnc" }
|
||||
$promptLines += ""
|
||||
$promptLines += "Enter the new Machine Number:"
|
||||
|
||||
$prompt = $promptLines -join "`n"
|
||||
$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($new)) {
|
||||
Write-Host "User cancelled. Will prompt again next logon."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
$new = $new.Trim()
|
||||
|
||||
# --- Validate ---
|
||||
if ($new -notmatch '^\d+$') {
|
||||
Write-Host "Invalid input: '$new' (not digits only). Will prompt again next logon."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Machine number must be digits only.`n`nYou entered: '$new'`n`nThe prompt will appear again at next logon.",
|
||||
"Invalid Machine Number",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Update via shared helper ---
|
||||
$mnResult = Update-MachineNumber -NewNumber $new -Site $site
|
||||
|
||||
$results = @()
|
||||
if ($mnResult.UdcUpdated) { $results += "UDC updated to $new" }
|
||||
if ($mnResult.EdncUpdated) { $results += "eDNC updated to $new" }
|
||||
foreach ($err in $mnResult.Errors) { $results += $err -replace '^', 'FAILED: ' }
|
||||
|
||||
# --- Show result ---
|
||||
$summary = ($results -join "`n") + "`n`nTo apply eDNC changes, restart any running DncMain.exe.`n`nFull log: C:\Logs\SFLD\Check-MachineNumber.log"
|
||||
# Force the MessageBox to topmost + take focus so it isn't hidden behind
|
||||
# other windows. Without this, the result dialog can render off-screen or
|
||||
# behind the FormTracePak / DNC windows and the tech misses it.
|
||||
$tmpForm = New-Object System.Windows.Forms.Form
|
||||
$tmpForm.TopMost = $true
|
||||
$tmpForm.WindowState = 'Minimized'
|
||||
$tmpForm.ShowInTaskbar = $false
|
||||
$tmpForm.Opacity = 0
|
||||
$tmpForm.Show()
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
$summary,
|
||||
"Machine Number Updated",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Information
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
|
||||
# --- Unregister task on success ---
|
||||
Write-Host "Results: $($results -join '; ')"
|
||||
$anyFail = $results | Where-Object { $_ -match 'FAILED' }
|
||||
if (-not $anyFail) {
|
||||
Write-Host "All updates succeeded. Unregistering logon task."
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
} else {
|
||||
Write-Host "Some updates failed. Task stays registered - will prompt again next logon."
|
||||
}
|
||||
|
||||
Write-Host "Check-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,23 +0,0 @@
|
||||
@echo off
|
||||
REM Configure-PC.bat - Launches Configure-PC.ps1 in an elevated PowerShell.
|
||||
REM SupportUser desktop tool for setting machine number + auto-startup items.
|
||||
|
||||
title Configure PC
|
||||
|
||||
set "SCRIPT=%~dp0Configure-PC.ps1"
|
||||
if exist "%SCRIPT%" goto :found
|
||||
|
||||
set "SCRIPT=C:\Users\SupportUser\Desktop\Configure-PC.ps1"
|
||||
if exist "%SCRIPT%" goto :found
|
||||
|
||||
set "SCRIPT=C:\Enrollment\shopfloor-setup\Shopfloor\Configure-PC.ps1"
|
||||
if exist "%SCRIPT%" goto :found
|
||||
|
||||
echo ERROR: Configure-PC.ps1 not found
|
||||
pause
|
||||
exit /b 1
|
||||
|
||||
:found
|
||||
echo Launching: %SCRIPT%
|
||||
powershell -NoProfile -Command "Start-Process powershell.exe -Verb RunAs -ArgumentList '-NoProfile','-NoExit','-ExecutionPolicy','Bypass','-File','%SCRIPT%'"
|
||||
exit /b
|
||||
@@ -1,504 +0,0 @@
|
||||
# Configure-PC.ps1 - Interactive tool for SupportUser to configure a
|
||||
# shopfloor PC after imaging. Two sections:
|
||||
#
|
||||
# 1. Machine number - if UDC/eDNC are still at placeholder 9999, prompt
|
||||
# to set the real number right now.
|
||||
#
|
||||
# 2. Auto-startup items + machine-number logon prompt - toggle which apps
|
||||
# start automatically for all users, and optionally register a logon
|
||||
# task that prompts STANDARD users for the machine number if it's
|
||||
# still 9999 when they log in (for the case where SupportUser skips
|
||||
# it here and the end user needs to set it themselves).
|
||||
#
|
||||
# Startup .lnk files go in the AllUsers Startup folder:
|
||||
# C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup\
|
||||
#
|
||||
# Run via Configure-PC.bat on the SupportUser desktop.
|
||||
|
||||
param(
|
||||
# When set, only show the machine number section (skip startup items).
|
||||
# Used by sync_intune -AsTask after completion -- startup items are
|
||||
# already auto-applied by 06-OrganizeDesktop from the PC profile.
|
||||
[switch]$MachineNumberOnly
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
# --- Transcript logging ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcriptPath = Join-Path $logDir 'Configure-PC.log'
|
||||
try { Start-Transcript -Path $transcriptPath -Append -Force | Out-Null } catch {}
|
||||
Write-Host "Transcript: $transcriptPath"
|
||||
Write-Host "Started: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
# Load site config + PC profile (resolves pc-type.txt + pc-subtype.txt
|
||||
# into a profile from site-config.json's pcProfiles section)
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Startup'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
$overridesPath = 'C:\ProgramData\GE\Shopfloor\startup-overrides.json'
|
||||
|
||||
# Persist user-toggled startup overrides so the logon-triggered sweep
|
||||
# (06-OrganizeDesktop.ps1 Phase 3) doesn't re-create .lnks the tech
|
||||
# explicitly removed. Labels in $suppressed are the ones currently OFF.
|
||||
function Get-SuppressedStartup {
|
||||
if (-not (Test-Path -LiteralPath $overridesPath)) { return @() }
|
||||
try {
|
||||
$data = Get-Content -LiteralPath $overridesPath -Raw | ConvertFrom-Json
|
||||
if ($data.suppressed) { return @($data.suppressed) }
|
||||
} catch {
|
||||
Write-Warning " Failed to parse $overridesPath : $_"
|
||||
}
|
||||
return @()
|
||||
}
|
||||
|
||||
function Set-SuppressedStartup {
|
||||
param([string[]]$Labels)
|
||||
$dir = Split-Path -Parent $overridesPath
|
||||
if (-not (Test-Path $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
}
|
||||
$payload = @{ suppressed = @($Labels | Sort-Object -Unique) }
|
||||
$payload | ConvertTo-Json | Set-Content -LiteralPath $overridesPath -Encoding UTF8
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Helpers
|
||||
# ============================================================================
|
||||
|
||||
function Get-UrlFromFile {
|
||||
param([string]$BaseName)
|
||||
$candidates = @(
|
||||
(Join-Path $publicDesktop "$BaseName.url"),
|
||||
(Join-Path (Join-Path $publicDesktop 'Web Links') "$BaseName.url")
|
||||
)
|
||||
foreach ($c in $candidates) {
|
||||
if (-not (Test-Path -LiteralPath $c)) { continue }
|
||||
try {
|
||||
$content = Get-Content -LiteralPath $c -ErrorAction Stop
|
||||
$urlLine = $content | Where-Object { $_ -match '^URL=' } | Select-Object -First 1
|
||||
if ($urlLine) { return ($urlLine -replace '^URL=', '').Trim() }
|
||||
} catch {}
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function New-StartupLnk {
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Target,
|
||||
[string]$Arguments = '',
|
||||
[string]$WorkingDirectory = ''
|
||||
)
|
||||
$path = Join-Path $startupDir "$Name.lnk"
|
||||
$wsh = New-Object -ComObject WScript.Shell
|
||||
try {
|
||||
$sc = $wsh.CreateShortcut($path)
|
||||
$sc.TargetPath = $Target
|
||||
if ($Arguments) { $sc.Arguments = $Arguments }
|
||||
if ($WorkingDirectory) { $sc.WorkingDirectory = $WorkingDirectory }
|
||||
elseif ($Target) {
|
||||
$parent = Split-Path -Parent $Target
|
||||
if ($parent) { $sc.WorkingDirectory = $parent }
|
||||
}
|
||||
$sc.Save()
|
||||
return $true
|
||||
} catch {
|
||||
Write-Warning "Failed to create startup shortcut '$Name': $_"
|
||||
return $false
|
||||
} finally {
|
||||
try { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($wsh) | Out-Null } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Machine number - read current state
|
||||
# ============================================================================
|
||||
|
||||
$currentMN = Get-CurrentMachineNumber
|
||||
$currentUdc = $currentMN.Udc
|
||||
$currentEdnc = $currentMN.Ednc
|
||||
|
||||
$needsMachineNumber = ($currentUdc -eq '9999' -or $currentEdnc -eq '9999')
|
||||
|
||||
# ============================================================================
|
||||
# Edge path (for URL-based startup items)
|
||||
# ============================================================================
|
||||
|
||||
$edgePath = @(
|
||||
'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe',
|
||||
'C:\Program Files\Microsoft\Edge\Application\msedge.exe'
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
# ============================================================================
|
||||
# Startup item definitions
|
||||
# Resolved from: pcProfile.startupItems > siteConfig.startupItems > hardcoded
|
||||
# ============================================================================
|
||||
|
||||
$cfgItems = Get-ProfileValue 'startupItems'
|
||||
|
||||
if ($null -ne $cfgItems -and $cfgItems.Count -gt 0) {
|
||||
$items = @()
|
||||
$num = 0
|
||||
foreach ($si in $cfgItems) {
|
||||
$num++
|
||||
switch ($si.type) {
|
||||
'exe' {
|
||||
$target = $si.target
|
||||
$items += @{
|
||||
Num = $num
|
||||
Label = $si.label
|
||||
Detail = (Split-Path -Leaf $target)
|
||||
Available = (Test-Path $target)
|
||||
CreateLnk = [scriptblock]::Create("return (New-StartupLnk -Name '$($si.label)' -Target '$target')")
|
||||
}
|
||||
}
|
||||
'existing' {
|
||||
$srcLnk = $si.sourceLnk
|
||||
$label = $si.label
|
||||
$items += @{
|
||||
Num = $num
|
||||
Label = $label
|
||||
Detail = 'shortcut'
|
||||
Available = $true
|
||||
CreateLnk = [scriptblock]::Create(@"
|
||||
`$src = @(
|
||||
(Join-Path `$publicDesktop '$srcLnk'),
|
||||
(Join-Path (Join-Path `$publicDesktop 'Shopfloor Tools') '$srcLnk')
|
||||
) | Where-Object { Test-Path -LiteralPath `$_ } | Select-Object -First 1
|
||||
if (`$src) {
|
||||
`$dst = Join-Path `$startupDir '$label.lnk'
|
||||
try { Copy-Item -LiteralPath `$src -Destination `$dst -Force; return `$true }
|
||||
catch { Write-Warning "Failed to copy $label`: `$_"; return `$false }
|
||||
} else {
|
||||
Write-Warning "$srcLnk not found on desktop"
|
||||
return `$false
|
||||
}
|
||||
"@)
|
||||
}
|
||||
}
|
||||
'url' {
|
||||
$urlKey = $si.urlKey
|
||||
$label = $si.label
|
||||
$fallback = if ($urlKey -and $siteConfig.urls) { $siteConfig.urls.$urlKey } else { '' }
|
||||
$items += @{
|
||||
Num = $num
|
||||
Label = $label
|
||||
Detail = 'opens in Edge'
|
||||
Available = [bool]$edgePath
|
||||
CreateLnk = [scriptblock]::Create(@"
|
||||
`$fallback = '$fallback'
|
||||
`$url = Get-UrlFromFile '$label'
|
||||
if (-not `$url) {
|
||||
Write-Host " $label .url not found on desktop, using known URL."
|
||||
`$url = `$fallback
|
||||
}
|
||||
Write-Host " URL: `$url"
|
||||
return (New-StartupLnk -Name '$label' -Target `$edgePath -Arguments "--new-window ```"`$url```"")
|
||||
"@)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$items = @(
|
||||
@{
|
||||
Num = 1
|
||||
Label = 'UDC'
|
||||
Detail = 'UDC.exe'
|
||||
Available = (Test-Path 'C:\Program Files\UDC\UDC.exe')
|
||||
CreateLnk = {
|
||||
return (New-StartupLnk -Name 'UDC' -Target 'C:\Program Files\UDC\UDC.exe')
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 2
|
||||
Label = 'eDNC'
|
||||
Detail = 'DncMain.exe'
|
||||
Available = (Test-Path 'C:\Program Files (x86)\Dnc\bin\DncMain.exe')
|
||||
CreateLnk = {
|
||||
return (New-StartupLnk -Name 'eDNC' -Target 'C:\Program Files (x86)\Dnc\bin\DncMain.exe')
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 3
|
||||
Label = 'Defect Tracker'
|
||||
Detail = 'ClickOnce app'
|
||||
Available = $true
|
||||
CreateLnk = {
|
||||
$src = @(
|
||||
(Join-Path $publicDesktop 'Defect_Tracker.lnk'),
|
||||
(Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'Defect_Tracker.lnk')
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
if ($src) {
|
||||
$dst = Join-Path $startupDir 'Defect Tracker.lnk'
|
||||
try { Copy-Item -LiteralPath $src -Destination $dst -Force; return $true }
|
||||
catch { Write-Warning "Failed to copy Defect Tracker: $_"; return $false }
|
||||
} else {
|
||||
Write-Warning "Defect_Tracker.lnk not found on desktop"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 4
|
||||
Label = 'WJ Shopfloor'
|
||||
Detail = 'HostExplorer session'
|
||||
Available = $true
|
||||
CreateLnk = {
|
||||
$src = @(
|
||||
(Join-Path $publicDesktop 'WJ Shopfloor.lnk'),
|
||||
(Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'WJ Shopfloor.lnk')
|
||||
) | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
if ($src) {
|
||||
$dst = Join-Path $startupDir 'WJ Shopfloor.lnk'
|
||||
try { Copy-Item -LiteralPath $src -Destination $dst -Force; return $true }
|
||||
catch { Write-Warning "Failed to copy WJ Shopfloor: $_"; return $false }
|
||||
} else {
|
||||
Write-Warning "WJ Shopfloor.lnk not found on desktop"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
}
|
||||
@{
|
||||
Num = 5
|
||||
Label = 'Plant Apps'
|
||||
Detail = 'opens in Edge'
|
||||
Available = [bool]$edgePath
|
||||
CreateLnk = {
|
||||
$fallback = 'https://mes-wjefferson.apps.lr.geaerospace.net/run/?app_name=Plant%20Applications'
|
||||
$url = Get-UrlFromFile 'Plant Apps'
|
||||
if (-not $url) {
|
||||
Write-Host " Plant Apps .url not found on desktop, using known URL."
|
||||
$url = $fallback
|
||||
}
|
||||
Write-Host " URL: $url"
|
||||
return (New-StartupLnk -Name 'Plant Apps' -Target $edgePath -Arguments "--new-window `"$url`"")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
# Machine-number logon tasks (item 6 toggle controls both)
|
||||
# 2026-05-24: split into user-context Prompt + SYSTEM-context Apply.
|
||||
# 'Check Machine Number' is the legacy single-task name kept for
|
||||
# backward-detection on bays imaged before the split.
|
||||
$machineNumPromptTask = 'Prompt Machine Number'
|
||||
$machineNumApplyTask = 'Apply Machine Number'
|
||||
$machineNumLegacyTask = 'Check Machine Number'
|
||||
|
||||
# ============================================================================
|
||||
# Interactive UI
|
||||
# ============================================================================
|
||||
|
||||
Clear-Host
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Configure PC" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
|
||||
# --- Section 1: Machine number ---
|
||||
Write-Host ""
|
||||
Write-Host " MACHINE NUMBER" -ForegroundColor Yellow
|
||||
Write-Host " ----------------------------------------"
|
||||
|
||||
if ($needsMachineNumber) {
|
||||
if ($currentUdc) { Write-Host " UDC : $currentUdc" -ForegroundColor Red }
|
||||
if ($currentEdnc) { Write-Host " eDNC : $currentEdnc" -ForegroundColor Red }
|
||||
Write-Host ""
|
||||
$newNum = Read-Host " Enter new machine number (digits only, Enter to skip)"
|
||||
$newNum = $newNum.Trim()
|
||||
|
||||
if ($newNum -and $newNum -match '^\d+$') {
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
$mnResult = Update-MachineNumber -NewNumber $newNum -Site $site
|
||||
|
||||
if ($mnResult.UdcUpdated) {
|
||||
Write-Host " UDC : $currentUdc -> $newNum" -ForegroundColor Green
|
||||
$currentUdc = $newNum
|
||||
}
|
||||
if ($mnResult.EdncUpdated) {
|
||||
Write-Host " eDNC : $currentEdnc -> $newNum" -ForegroundColor Green
|
||||
$currentEdnc = $newNum
|
||||
}
|
||||
foreach ($err in $mnResult.Errors) { Write-Warning " $err" }
|
||||
if ($mnResult.UdcUpdated) { Write-Host " UDC.exe relaunched." }
|
||||
|
||||
$needsMachineNumber = ($currentUdc -eq '9999' -or $currentEdnc -eq '9999')
|
||||
} elseif ($newNum) {
|
||||
Write-Host " Invalid (digits only). Skipped." -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host " Skipped (still 9999)." -ForegroundColor DarkGray
|
||||
}
|
||||
} else {
|
||||
if ($currentUdc) { Write-Host " UDC : $currentUdc" -ForegroundColor Green }
|
||||
if ($currentEdnc) { Write-Host " eDNC : $currentEdnc" -ForegroundColor Green }
|
||||
}
|
||||
|
||||
# --- Section 2: Startup items + machine-number logon task ---
|
||||
# Skipped when -MachineNumberOnly (called from sync_intune -AsTask after
|
||||
# completion -- startup items already auto-applied by 06-OrganizeDesktop).
|
||||
if (-not $MachineNumberOnly) {
|
||||
Write-Host ""
|
||||
Write-Host " AUTO-STARTUP ITEMS" -ForegroundColor Yellow
|
||||
Write-Host " ----------------------------------------"
|
||||
Write-Host " Toggle which items start at user logon:"
|
||||
Write-Host ""
|
||||
|
||||
$existingStartup = @(Get-ChildItem -LiteralPath $startupDir -Filter '*.lnk' -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BaseName)
|
||||
|
||||
foreach ($item in $items) {
|
||||
$on = if ($existingStartup -contains $item.Label) { '[ON]' } else { '[ ]' }
|
||||
$avail = if ($item.Available) { '' } else { ' (not installed)' }
|
||||
Write-Host " $($item.Num). $on $($item.Label) - $($item.Detail)$avail"
|
||||
}
|
||||
|
||||
# Item 6: machine number logon prompt. "ON" if EITHER the new Prompt task OR
|
||||
# the legacy Check Machine Number task is registered.
|
||||
$machineNumTaskExists = [bool](
|
||||
(Get-ScheduledTask -TaskName $machineNumPromptTask -ErrorAction SilentlyContinue) -or
|
||||
(Get-ScheduledTask -TaskName $machineNumLegacyTask -ErrorAction SilentlyContinue)
|
||||
)
|
||||
$mnOn = if ($machineNumTaskExists) { '[ON]' } else { '[ ]' }
|
||||
Write-Host " 6. $mnOn Prompt standard user for machine number if 9999"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host " Enter numbers to toggle (e.g. 1,2,6), Enter to keep current:"
|
||||
$selection = Read-Host " Selection"
|
||||
$selection = $selection.Trim()
|
||||
|
||||
if ($selection) {
|
||||
$selected = $selection -split '[,\s]+' | Where-Object { $_ -match '^\d+$' } | ForEach-Object { [int]$_ }
|
||||
|
||||
# Process startup items 1-5
|
||||
$suppressed = @(Get-SuppressedStartup)
|
||||
$suppressedChanged = $false
|
||||
|
||||
foreach ($item in $items) {
|
||||
if ($selected -notcontains $item.Num) { continue }
|
||||
|
||||
$existingLnk = Join-Path $startupDir "$($item.Label).lnk"
|
||||
if (Test-Path -LiteralPath $existingLnk) {
|
||||
# Toggling OFF: remove the .lnk AND record the suppression so
|
||||
# the logon sweep in 06-OrganizeDesktop doesn't re-create it.
|
||||
try {
|
||||
Remove-Item -LiteralPath $existingLnk -Force
|
||||
Write-Host " $($item.Label): REMOVED from startup" -ForegroundColor Yellow
|
||||
} catch { Write-Warning " Failed to remove $($item.Label): $_" }
|
||||
|
||||
if ($suppressed -notcontains $item.Label) {
|
||||
$suppressed += $item.Label
|
||||
$suppressedChanged = $true
|
||||
}
|
||||
} else {
|
||||
# Toggling ON: drop any suppression first so the sweep won't
|
||||
# fight us next logon, then create the .lnk.
|
||||
if ($suppressed -contains $item.Label) {
|
||||
$suppressed = @($suppressed | Where-Object { $_ -ne $item.Label })
|
||||
$suppressedChanged = $true
|
||||
}
|
||||
|
||||
if (-not $item.Available) {
|
||||
Write-Host " $($item.Label): not installed, cannot add to startup" -ForegroundColor DarkGray
|
||||
continue
|
||||
}
|
||||
|
||||
$result = & $item.CreateLnk
|
||||
if ($result) {
|
||||
Write-Host " $($item.Label): ADDED to startup" -ForegroundColor Green
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($suppressedChanged) {
|
||||
try {
|
||||
Set-SuppressedStartup -Labels $suppressed
|
||||
Write-Host " (saved override state to $overridesPath)" -ForegroundColor DarkGray
|
||||
} catch {
|
||||
Write-Warning " Failed to write override state: $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Process item 6: machine number logon task
|
||||
if ($selected -contains 6) {
|
||||
if ($machineNumTaskExists) {
|
||||
# Toggle OFF - remove Prompt + Apply (new design) AND the legacy
|
||||
# Check Machine Number task name (in case this bay was imaged
|
||||
# before the split and never re-imaged).
|
||||
$removed = @()
|
||||
foreach ($t in @($machineNumPromptTask, $machineNumApplyTask, $machineNumLegacyTask)) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop
|
||||
$removed += $t
|
||||
}
|
||||
} catch { Write-Warning " Failed to remove '$t': $_" }
|
||||
}
|
||||
if ($removed) {
|
||||
Write-Host " Machine number logon prompt: REMOVED ($($removed -join ', '))" -ForegroundColor Yellow
|
||||
}
|
||||
$machineNumTaskExists = $false
|
||||
} else {
|
||||
# Toggle ON - register logon task
|
||||
# Defer task registration to the shared registrar so this code
|
||||
# path always matches the imaging-time path. Registrar installs
|
||||
# BOTH the user-context "Prompt Machine Number" task and the
|
||||
# SYSTEM-context "Apply Machine Number" task, sets the SDDL on
|
||||
# Apply so Limited users can schtasks /run it, and cleans up
|
||||
# any legacy "Check Machine Number" task name.
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$registrar = Join-Path $scriptDir 'Register-CheckMachineNumberTask.ps1'
|
||||
if (-not (Test-Path -LiteralPath $registrar)) {
|
||||
$registrar = 'C:\Enrollment\shopfloor-setup\Shopfloor\Register-CheckMachineNumberTask.ps1'
|
||||
}
|
||||
if (Test-Path -LiteralPath $registrar) {
|
||||
try {
|
||||
& $registrar
|
||||
Write-Host " Machine number logon prompt: ENABLED" -ForegroundColor Green
|
||||
Write-Host " (Prompt user-task + Apply SYSTEM-task registered;" -ForegroundColor DarkGray
|
||||
Write-Host " will auto-disable after machine number is set)" -ForegroundColor DarkGray
|
||||
$machineNumTaskExists = $true
|
||||
} catch {
|
||||
Write-Warning " Register-CheckMachineNumberTask failed: $_"
|
||||
}
|
||||
} else {
|
||||
Write-Warning " Register-CheckMachineNumberTask.ps1 not found at $registrar"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Write-Host " No changes." -ForegroundColor DarkGray
|
||||
}
|
||||
|
||||
} # end if (-not $MachineNumberOnly)
|
||||
|
||||
# --- Summary ---
|
||||
Write-Host ""
|
||||
Write-Host " CURRENT STATE" -ForegroundColor Cyan
|
||||
Write-Host " ----------------------------------------"
|
||||
$final = @(Get-ChildItem -LiteralPath $startupDir -Filter '*.lnk' -File -ErrorAction SilentlyContinue | Select-Object -ExpandProperty BaseName)
|
||||
Write-Host " Startup items:"
|
||||
if ($final.Count -eq 0) {
|
||||
Write-Host " (none)"
|
||||
} else {
|
||||
foreach ($f in $final) { Write-Host " - $f" }
|
||||
}
|
||||
$mnState = if ($machineNumTaskExists) { 'ON (prompts at logon if 9999)' } else { 'OFF' }
|
||||
Write-Host " Machine number logon prompt: $mnState"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " Done" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
Write-Host "Completed: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
|
||||
Write-Host "Press any key to close..."
|
||||
try { [void][Console]::ReadKey($true) } catch { [void](Read-Host) }
|
||||
@@ -70,6 +70,10 @@ if "%RC%"=="0" (
|
||||
if not exist "C:\Enrollment" mkdir "C:\Enrollment"
|
||||
> "C:\Enrollment\force-lockdown-applied.txt" echo %DATE% %TIME%
|
||||
echo Marker written: C:\Enrollment\force-lockdown-applied.txt
|
||||
REM Clear the imaging AutoLogonCount: Autologon leaves it in place and
|
||||
REM ShopFloor autologon switches itself off when it counts down to 0.
|
||||
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /f /reg:64 >nul 2>&1
|
||||
echo Cleared leftover AutoLogonCount.
|
||||
)
|
||||
|
||||
echo.
|
||||
|
||||
364
playbook/shopfloor-setup/Shopfloor/Install-AppList.ps1
Normal file
364
playbook/shopfloor-setup/Shopfloor/Install-AppList.ps1
Normal file
@@ -0,0 +1,364 @@
|
||||
# Install-AppList.ps1 - install the apps in a site app list (shopfloor).
|
||||
#
|
||||
# -ListPath C:\PreInstall\preinstall.json during setup (Run-ShopfloorSetup,
|
||||
# same logon as the PPKG)
|
||||
# C:\PreInstall\postppkg.json after the post-PPKG reboot
|
||||
# (Invoke-PostPpkg.ps1, SYSTEM)
|
||||
# -Phase name for the log folder: C:\Logs\<Phase>\install.log
|
||||
#
|
||||
# Both lists are EMPTY in the generic build; a site adds its apps:
|
||||
# { "Name": "Foo 1.2", "Installer": "foo\\Foo.msi", "Type": "MSI" | "EXE",
|
||||
# "InstallArgs": "/qn /norestart",
|
||||
# "DetectionMethod": "Registry" | "File", "DetectionPath": "...",
|
||||
# optional: "DetectionName", "DetectionValue", "LogFile",
|
||||
# "KillAfterDetection": true, "PCTypes": ["gea-shopfloor-standard"] }
|
||||
# Installers come from <list folder>\installers\ (staged from the PXE server's
|
||||
# enrollment share, pre-install\installers\). Detection runs first, so an app
|
||||
# that is already there is skipped and re-runs are safe.
|
||||
#
|
||||
# Never fails the caller: a missing or empty list, a bad JSON file or a failed
|
||||
# install is logged and the script exits 0.
|
||||
|
||||
param(
|
||||
[string]$ListPath = 'C:\PreInstall\preinstall.json',
|
||||
[string]$Phase = 'PreInstall'
|
||||
)
|
||||
|
||||
$installerDir = Join-Path (Split-Path -Parent $ListPath) 'installers'
|
||||
$logDir = "C:\Logs\$Phase"
|
||||
$logFile = Join-Path $logDir 'install.log'
|
||||
|
||||
# Appended, never wiped: an installer-triggered reboot can kill a run, and the
|
||||
# next run must not destroy that record.
|
||||
if (-not (Test-Path $logDir)) {
|
||||
New-Item -Path $logDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
function Write-ListLog {
|
||||
param(
|
||||
[Parameter(Mandatory=$true, Position=0)]
|
||||
[string]$Message,
|
||||
[Parameter(Position=1)]
|
||||
[ValidateSet('INFO','WARN','ERROR')]
|
||||
[string]$Level = 'INFO'
|
||||
)
|
||||
$stamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||
$line = "[$stamp] [$Level] $Message"
|
||||
Write-Host $line
|
||||
# Write-through, so each line is on disk even if a reboot follows.
|
||||
try {
|
||||
$fs = New-Object System.IO.FileStream(
|
||||
$logFile,
|
||||
[System.IO.FileMode]::Append,
|
||||
[System.IO.FileAccess]::Write,
|
||||
[System.IO.FileShare]::Read,
|
||||
4096,
|
||||
[System.IO.FileOptions]::WriteThrough
|
||||
)
|
||||
$bytes = [System.Text.Encoding]::UTF8.GetBytes($line + "`r`n")
|
||||
$fs.Write($bytes, 0, $bytes.Length)
|
||||
$fs.Flush()
|
||||
$fs.Dispose()
|
||||
}
|
||||
catch {
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-ListLog "================================================================"
|
||||
Write-ListLog "=== $Phase session start (PID $PID): $ListPath ==="
|
||||
Write-ListLog "================================================================"
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ListPath)) {
|
||||
Write-ListLog "No app list at $ListPath - nothing to install."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$pcType = ''
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
if (Test-Path $pcTypeFile) {
|
||||
$pcType = "$(Get-Content $pcTypeFile -First 1 -ErrorAction SilentlyContinue)".Trim()
|
||||
}
|
||||
Write-ListLog "PC type: $(if ($pcType) { $pcType } else { '(unknown)' })"
|
||||
|
||||
try {
|
||||
$config = Get-Content -LiteralPath $ListPath -Raw | ConvertFrom-Json
|
||||
} catch {
|
||||
Write-ListLog "Failed to parse $ListPath : $_" "ERROR"
|
||||
exit 0
|
||||
}
|
||||
|
||||
$apps = @($config.Applications | Where-Object { $_ })
|
||||
if ($apps.Count -eq 0) {
|
||||
Write-ListLog "The list has no applications - nothing to install."
|
||||
exit 0
|
||||
}
|
||||
Write-ListLog "Installer dir: $installerDir"
|
||||
Write-ListLog "$($apps.Count) app entries"
|
||||
|
||||
# --- Detection helper ---
|
||||
function Test-AppInstalled {
|
||||
param($App)
|
||||
|
||||
if (-not $App.DetectionMethod) {
|
||||
return $false
|
||||
}
|
||||
|
||||
try {
|
||||
switch ($App.DetectionMethod) {
|
||||
"Registry" {
|
||||
if (-not (Test-Path $App.DetectionPath)) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionName) {
|
||||
$value = Get-ItemProperty -Path $App.DetectionPath -Name $App.DetectionName -ErrorAction SilentlyContinue
|
||||
if (-not $value) {
|
||||
return $false
|
||||
}
|
||||
if ($App.DetectionValue) {
|
||||
return ($value.$($App.DetectionName) -eq $App.DetectionValue)
|
||||
}
|
||||
return $true
|
||||
}
|
||||
return $true
|
||||
}
|
||||
"File" {
|
||||
return Test-Path $App.DetectionPath
|
||||
}
|
||||
default {
|
||||
Write-ListLog " Unknown detection method: $($App.DetectionMethod)" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-ListLog " Detection check threw: $_" "WARN"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
# --- Iterate apps ---
|
||||
$installed = 0
|
||||
$skipped = 0
|
||||
$failed = 0
|
||||
|
||||
foreach ($app in $apps) {
|
||||
# Cancel any reboot a previous installer scheduled (some respect /norestart, some
|
||||
# don't - VC++ 2008's bootstrapper sometimes triggers an immediate Windows reboot
|
||||
# despite the flag). Doing this BEFORE each install protects the rest of the loop.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
Write-ListLog "==> $($app.Name)"
|
||||
|
||||
# PCTypes: absent, '*' or the PC's type = install. (One generic type,
|
||||
# gea-shopfloor-standard; the filter is there for sites that add types.)
|
||||
$allowedTypes = @($app.PCTypes | Where-Object { $_ })
|
||||
if ($allowedTypes.Count -gt 0 -and -not ($allowedTypes -contains '*') -and -not ($allowedTypes -icontains $pcType)) {
|
||||
Write-ListLog " PCTypes excludes '$pcType' (allowed: $($allowedTypes -join ', ')) - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Detection check
|
||||
if (Test-AppInstalled -App $app) {
|
||||
Write-ListLog " Already installed - skipping"
|
||||
$skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
# Locate installer file
|
||||
$installerPath = Join-Path $installerDir $app.Installer
|
||||
if (-not (Test-Path $installerPath)) {
|
||||
Write-ListLog " Installer file not found: $installerPath" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
Write-ListLog " Installing from $installerPath"
|
||||
if ($app.InstallArgs) {
|
||||
Write-ListLog " InstallArgs: $($app.InstallArgs)"
|
||||
}
|
||||
|
||||
# Per-app verbose msiexec log path - written by /L*v on MSI installs and tailed
|
||||
# into the runner log on failure so we don't need to dig through C:\Logs manually.
|
||||
$safeName = $app.Name -replace '[^a-zA-Z0-9]','_'
|
||||
$msiLog = Join-Path $logDir "msi-$safeName.log"
|
||||
if (Test-Path $msiLog) { Remove-Item $msiLog -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
try {
|
||||
# We use [System.Diagnostics.Process]::Start() directly instead of Start-Process
|
||||
# because PowerShell 5.1's Start-Process -PassThru has a bug where the returned
|
||||
# Process object's OS handle is disposed when control returns to the script,
|
||||
# causing ExitCode to always read as $null even after WaitForExit() succeeds.
|
||||
# Calling Process.Start() directly returns a Process object with a live handle
|
||||
# that exposes ExitCode correctly. We poll HasExited so UDC_Setup.exe (which
|
||||
# hangs forever after spawning a hidden WPF window) can be killed via the
|
||||
# detection-during-install path.
|
||||
$psi = New-Object System.Diagnostics.ProcessStartInfo
|
||||
$psi.UseShellExecute = $false
|
||||
$psi.CreateNoWindow = $true
|
||||
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
|
||||
|
||||
if ($app.Type -eq "MSI") {
|
||||
$psi.FileName = "msiexec.exe"
|
||||
$psi.Arguments = "/i `"$installerPath`""
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments += " " + $app.InstallArgs
|
||||
}
|
||||
$psi.Arguments += " /L*v `"$msiLog`""
|
||||
Write-ListLog " msiexec verbose log: $msiLog"
|
||||
}
|
||||
elseif ($app.Type -eq "EXE") {
|
||||
$psi.FileName = $installerPath
|
||||
if ($app.InstallArgs) {
|
||||
$psi.Arguments = $app.InstallArgs
|
||||
}
|
||||
# If the JSON entry specifies a LogFile (per-installer log written by
|
||||
# the EXE itself - e.g. Setup-OpenText.ps1 -> C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), surface it on failure the same way we surface the
|
||||
# msiexec /L*v verbose log for MSI installs. Lets EXE wrappers actually
|
||||
# report what went wrong inside.
|
||||
if ($app.LogFile) {
|
||||
Write-ListLog " Installer log: $($app.LogFile)"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-ListLog " Unsupported Type: $($app.Type) - skipping" "ERROR"
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
# 1618 = another MSI install is running (after the PPKG its own
|
||||
# installs can still be going). Wait and retry, up to 5 times.
|
||||
for ($try = 1; $app.Type -eq "MSI" -and $try -le 5; $try++) {
|
||||
$proc.WaitForExit(15000) | Out-Null
|
||||
if (-not $proc.HasExited -or $proc.ExitCode -ne 1618) { break }
|
||||
Write-ListLog " Another installation is in progress (1618) - retry $try of 5 in 60 s" "WARN"
|
||||
Start-Sleep -Seconds 60
|
||||
$proc = [System.Diagnostics.Process]::Start($psi)
|
||||
}
|
||||
|
||||
# Poll for completion: process exit OR detection success (whichever happens first)
|
||||
$timeoutSec = 600 # 10 min hard cap per app
|
||||
$pollInterval = 5
|
||||
$elapsed = 0
|
||||
$killedAfterDetect = $false
|
||||
|
||||
while ($elapsed -lt $timeoutSec) {
|
||||
if ($proc.HasExited) { break }
|
||||
|
||||
# Detection-during-install kill is OPT-IN via "KillAfterDetection: true"
|
||||
# in the JSON entry. It's only safe for installers that hang forever after
|
||||
# creating their detection markers (UDC_Setup.exe spawns a hidden WPF window
|
||||
# and never exits). For normal installers (Oracle, msiexec, etc.) the
|
||||
# detection registry key often gets written midway through the install AND
|
||||
# msiexec is still doing post-install cleanup AND msiserver is still holding
|
||||
# the install mutex - killing msiexec at that point leaves the system in a
|
||||
# bad state and the NEXT msiexec call returns 1618 ERROR_INSTALL_ALREADY_
|
||||
# RUNNING. So opt-out by default; only kill apps that explicitly need it.
|
||||
if ($app.KillAfterDetection -and (Test-AppInstalled -App $app)) {
|
||||
Write-ListLog " Detection passed at $elapsed s - killing installer to advance (KillAfterDetection)"
|
||||
try { $proc.Kill(); $proc.WaitForExit(5000) | Out-Null } catch { }
|
||||
|
||||
$killedAfterDetect = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $pollInterval
|
||||
$elapsed += $pollInterval
|
||||
}
|
||||
|
||||
if (-not $proc.HasExited -and -not $killedAfterDetect) {
|
||||
Write-ListLog " TIMEOUT after $timeoutSec seconds - killing installer" "ERROR"
|
||||
try { $proc.Kill() } catch { }
|
||||
$failed++
|
||||
continue
|
||||
}
|
||||
|
||||
if ($killedAfterDetect) {
|
||||
Write-ListLog " SUCCESS (verified via detection during install)"
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
# Start-Process -PassThru returns a Process object whose ExitCode is null
|
||||
# until WaitForExit() is called - even if HasExited is already true. Without
|
||||
# this, every install that goes through the exit-code branch gets reported
|
||||
# as "Exit code - FAILED" (empty) regardless of the real result.
|
||||
try { $proc.WaitForExit() } catch { }
|
||||
$exitCode = $proc.ExitCode
|
||||
|
||||
if ($exitCode -eq 0 -or $exitCode -eq 3010) {
|
||||
Write-ListLog " Exit code $exitCode after $elapsed s - SUCCESS"
|
||||
if ($exitCode -eq 3010) {
|
||||
Write-ListLog " (Reboot required for $($app.Name))"
|
||||
}
|
||||
$installed++
|
||||
}
|
||||
else {
|
||||
Write-ListLog " Exit code $exitCode - FAILED" "ERROR"
|
||||
|
||||
# If the JSON entry specifies a LogFile for an EXE install (e.g.
|
||||
# Setup-OpenText.ps1 writes its own log at C:\Logs\PreInstall\Setup-
|
||||
# OpenText.log), tail it here so the runner log surfaces the actual
|
||||
# cause without us having to dig through C:\Logs manually.
|
||||
if ($app.Type -eq "EXE" -and $app.LogFile -and (Test-Path $app.LogFile)) {
|
||||
Write-ListLog " --- last 30 lines of $($app.LogFile) ---"
|
||||
Get-Content $app.LogFile -Tail 30 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-ListLog " $_"
|
||||
}
|
||||
Write-ListLog " --- end installer log tail ---"
|
||||
}
|
||||
|
||||
# Surface the *meaningful* lines from the verbose msiexec log (not just
|
||||
# the tail, which is rollback/cleanup noise). The actual root-cause lines
|
||||
# are: MSI error notes (Note: 1: <code>), failed action returns (return
|
||||
# value 3), errors logged by custom actions, and any line containing the
|
||||
# word "Error". Grep these out so the runner log shows the smoking gun.
|
||||
if ($app.Type -eq "MSI" -and (Test-Path $msiLog)) {
|
||||
Write-ListLog " --- meaningful lines from $msiLog ---"
|
||||
$patterns = @(
|
||||
'Note: 1: ',
|
||||
'return value 3',
|
||||
'Error \d+\.',
|
||||
'CustomAction .* returned actual error',
|
||||
'Failed to ',
|
||||
'Installation failed',
|
||||
'Calling custom action',
|
||||
'1: 2262', # Database not found
|
||||
'1: 2203', # Cannot open database
|
||||
'1: 2330' # Insufficient permissions
|
||||
)
|
||||
$regex = ($patterns -join '|')
|
||||
$matches = Select-String -Path $msiLog -Pattern $regex -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 30
|
||||
if ($matches) {
|
||||
foreach ($m in $matches) {
|
||||
Write-ListLog " $($m.Line.Trim())"
|
||||
}
|
||||
} else {
|
||||
Write-ListLog " (no error patterns matched - falling back to last 25 lines)"
|
||||
Get-Content $msiLog -Tail 25 -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-ListLog " $_"
|
||||
}
|
||||
}
|
||||
Write-ListLog " --- end MSI log scan ---"
|
||||
}
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-ListLog " Install threw: $_" "ERROR"
|
||||
$failed++
|
||||
}
|
||||
}
|
||||
|
||||
Write-ListLog "============================================"
|
||||
Write-ListLog "$Phase complete: $installed installed, $skipped skipped, $failed failed"
|
||||
Write-ListLog "============================================"
|
||||
|
||||
# Final reboot cancel - if the last installer in the loop scheduled one, the
|
||||
# caller's later `shutdown /a` may not come in time. Cancel here so control
|
||||
# returns cleanly to the caller.
|
||||
cmd /c "shutdown /a 2>nul" *>$null
|
||||
|
||||
exit 0
|
||||
86
playbook/shopfloor-setup/Shopfloor/Invoke-PostPpkg.ps1
Normal file
86
playbook/shopfloor-setup/Shopfloor/Invoke-PostPpkg.ps1
Normal file
@@ -0,0 +1,86 @@
|
||||
# Invoke-PostPpkg.ps1 - run by the "GE Shopfloor Post-PPKG" task (SYSTEM,
|
||||
# at startup and logon). Three jobs, each done once:
|
||||
#
|
||||
# 1. Install the site's post-PPKG app list, C:\PreInstall\postppkg.json
|
||||
# (Install-AppList.ps1; empty in the generic build). Only once the PPKG
|
||||
# has been applied (C:\Enrollment\.ppkg-applied, written by
|
||||
# run-enrollment.ps1), i.e. on the boot after the PPKG reboot.
|
||||
# Done marker: C:\Enrollment\postppkg-done.txt
|
||||
# 2. After the ShopFloor handover (Winlogon DefaultUserName ShopFloor*),
|
||||
# delete the imaging AutoLogonCount. Left in place, ShopFloor inherits
|
||||
# it and Windows turns autologon off when it reaches 0: the bay stops at
|
||||
# a ShopFloor password prompt a few reboots later. The monitor tries the
|
||||
# same, but it runs as Users and the HKLM write can be denied.
|
||||
# 3. Once Monitor-IntuneProgress has written C:\Enrollment\sync-complete.txt,
|
||||
# remove the "Shopfloor Intune Sync" logon task.
|
||||
# When all three are done the task unregisters itself.
|
||||
#
|
||||
# Never throws: every step logs and the script exits 0.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
New-Item -Path $logDir -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
try { Start-Transcript -Path (Join-Path $logDir 'postppkg.log') -Append -Force | Out-Null } catch {}
|
||||
|
||||
$appliedMarker = 'C:\Enrollment\.ppkg-applied'
|
||||
$doneMarker = 'C:\Enrollment\postppkg-done.txt'
|
||||
$syncComplete = 'C:\Enrollment\sync-complete.txt'
|
||||
$listPath = 'C:\PreInstall\postppkg.json'
|
||||
$taskName = 'GE Shopfloor Post-PPKG'
|
||||
$syncTask = 'Shopfloor Intune Sync'
|
||||
|
||||
Write-Host "=== Invoke-PostPpkg $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') ==="
|
||||
|
||||
# 1. post-PPKG app list
|
||||
if (Test-Path -LiteralPath $doneMarker) {
|
||||
Write-Host "Post-PPKG list already done ($(Get-Content $doneMarker -First 1))."
|
||||
} elseif (-not (Test-Path -LiteralPath $appliedMarker)) {
|
||||
Write-Host "PPKG not applied yet ($appliedMarker missing) - waiting for the next boot."
|
||||
} else {
|
||||
$runner = Join-Path $PSScriptRoot 'Install-AppList.ps1'
|
||||
if (Test-Path -LiteralPath $runner) {
|
||||
try { & $runner -ListPath $listPath -Phase 'PostPpkg' } catch { Write-Warning "Install-AppList failed: $_" }
|
||||
Set-Content -Path $doneMarker -Value (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') -ErrorAction SilentlyContinue
|
||||
Write-Host "Post-PPKG list done (log: C:\Logs\PostPpkg\install.log)."
|
||||
} else {
|
||||
Write-Warning "Install-AppList.ps1 not found next to this script ($runner)."
|
||||
}
|
||||
}
|
||||
|
||||
# 2. AutoLogonCount after the ShopFloor handover
|
||||
$handedOver = $false
|
||||
$countGone = $true
|
||||
try {
|
||||
$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||
$wl = Get-ItemProperty $wlKey -ErrorAction Stop
|
||||
$handedOver = ($wl.DefaultUserName -like 'ShopFloor*')
|
||||
if ($handedOver -and $null -ne $wl.AutoLogonCount) {
|
||||
Remove-ItemProperty $wlKey -Name AutoLogonCount -ErrorAction Stop
|
||||
Write-Host "Removed leftover AutoLogonCount ($($wl.AutoLogonCount)) after the ShopFloor handover."
|
||||
}
|
||||
$countGone = ($null -eq (Get-ItemProperty $wlKey -ErrorAction Stop).AutoLogonCount)
|
||||
} catch {
|
||||
Write-Warning "AutoLogonCount check failed: $_"
|
||||
$countGone = $false
|
||||
}
|
||||
|
||||
# 3. the monitor's logon task, once it has finished
|
||||
$syncDone = Test-Path -LiteralPath $syncComplete
|
||||
if ($syncDone -and (Get-ScheduledTask -TaskName $syncTask -ErrorAction SilentlyContinue)) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $syncTask -Confirm:$false -ErrorAction Stop
|
||||
Write-Host "Removed the '$syncTask' task (sync-complete.txt present)."
|
||||
} catch {
|
||||
Write-Warning "Could not remove '$syncTask': $_"
|
||||
}
|
||||
}
|
||||
|
||||
if ((Test-Path -LiteralPath $doneMarker) -and $handedOver -and $countGone -and $syncDone) {
|
||||
Write-Host "All done - unregistering '$taskName'."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction Stop } catch { Write-Warning "Could not unregister: $_" }
|
||||
} else {
|
||||
Write-Host ("Still to do: list={0} handover={1} autologoncount_cleared={2} sync_complete={3}" -f `
|
||||
(Test-Path -LiteralPath $doneMarker), $handedOver, $countGone, $syncDone)
|
||||
}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,200 +0,0 @@
|
||||
# Prompt-MachineNumber.ps1 - User-context GUI script for the two-task
|
||||
# machine number flow. Triggered AtLogOn for any BUILTIN\Users member.
|
||||
#
|
||||
# Flow:
|
||||
# 1. Read current UDC + eDNC values (read-only - no privileges needed).
|
||||
# 2. If neither is 9999, unregister self and exit (this PC is set up).
|
||||
# 3. Show InputBox for new machine number.
|
||||
# 4. Write number to C:\Logs\SFLD\machine-number-request.txt.
|
||||
# 5. Trigger the SYSTEM-context Apply-MachineNumber task via
|
||||
# schtasks /run. SYSTEM has full HKLM + ProgramData access so the
|
||||
# actual write happens with proper privileges - the prompted user
|
||||
# never needs HKLM write rights (security improvement over the old
|
||||
# 02-MachineNumberACLs.ps1 ACL-grant hack).
|
||||
# 6. Poll for C:\Logs\SFLD\machine-number-result.json (30s timeout).
|
||||
# 7. Show result MessageBox. Unregister self on success.
|
||||
#
|
||||
# Why this script doesn't do the writes itself: GUI is required (InputBox),
|
||||
# but GUI requires user-context (SYSTEM can't render to user desktop on
|
||||
# modern Windows). The user-context dialog gathers input; the SYSTEM task
|
||||
# does privileged writes.
|
||||
|
||||
# --- Transcript ---
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path -LiteralPath $logDir)) {
|
||||
try { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$transcript = Join-Path $logDir 'Prompt-MachineNumber.log'
|
||||
try { Start-Transcript -Path $transcript -Append -Force | Out-Null } catch {}
|
||||
Write-Host "Prompt-MachineNumber.ps1 starting $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
Write-Host "Running as: $([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)"
|
||||
|
||||
. "$PSScriptRoot\lib\Get-PCProfile.ps1"
|
||||
. "$PSScriptRoot\lib\Update-MachineNumber.ps1"
|
||||
|
||||
Add-Type -AssemblyName Microsoft.VisualBasic
|
||||
Add-Type -AssemblyName System.Windows.Forms
|
||||
|
||||
$taskName = 'Prompt Machine Number'
|
||||
$applyTaskName = 'Apply Machine Number'
|
||||
$requestFile = Join-Path $logDir 'machine-number-request.txt'
|
||||
$resultFile = Join-Path $logDir 'machine-number-result.json'
|
||||
$site = if ($siteConfig) { $siteConfig.siteName } else { 'West Jefferson' }
|
||||
|
||||
# --- Read current values (read-only, no perms needed) ---
|
||||
$currentMN = Get-CurrentMachineNumber
|
||||
$currentUdc = $currentMN.Udc
|
||||
$currentEdnc = $currentMN.Ednc
|
||||
Write-Host "UDC machine number: $(if ($currentUdc) { $currentUdc } else { '(not found)' })"
|
||||
Write-Host "eDNC machine number: $(if ($currentEdnc) { $currentEdnc } else { '(not found)' })"
|
||||
|
||||
if ($currentUdc -ne '9999' -and $currentEdnc -ne '9999') {
|
||||
Write-Host "Machine number is set (not 9999). Unregistering Prompt task and exiting."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
Write-Host "Placeholder 9999 detected - showing prompt."
|
||||
|
||||
# --- Show prompt ---
|
||||
$promptLines = @()
|
||||
$promptLines += "The machine number on this PC is still set to the"
|
||||
$promptLines += "placeholder value (9999). Please enter the correct"
|
||||
$promptLines += "machine number for this workstation."
|
||||
$promptLines += ""
|
||||
if ($currentUdc) { $promptLines += "Current UDC: $currentUdc" }
|
||||
if ($currentEdnc) { $promptLines += "Current eDNC: $currentEdnc" }
|
||||
$promptLines += ""
|
||||
$promptLines += "Enter the new Machine Number:"
|
||||
$prompt = $promptLines -join "`n"
|
||||
$new = [Microsoft.VisualBasic.Interaction]::InputBox($prompt, "Set Machine Number", "")
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($new)) {
|
||||
Write-Host "User cancelled. Will prompt again next logon."
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
$new = $new.Trim()
|
||||
|
||||
if ($new -notmatch '^\d+$') {
|
||||
Write-Host "Invalid input: '$new' (not digits only). Showing error and re-prompting next logon."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Machine number must be digits only.`n`nYou entered: '$new'`n`nThe prompt will appear again at next logon.",
|
||||
"Invalid Machine Number",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
}
|
||||
|
||||
# --- Hand off to SYSTEM task ---
|
||||
# Clean any stale request / result files first so we read fresh ones.
|
||||
Remove-Item -LiteralPath $requestFile, $resultFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
try {
|
||||
Set-Content -LiteralPath $requestFile -Value $new -Encoding ascii -Force -ErrorAction Stop
|
||||
} catch {
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
"Could not write request file at $requestFile`n`n$_`n`nThe prompt will appear again at next logon.",
|
||||
"Machine Number Request Failed",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Error
|
||||
) | Out-Null
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Wrote $requestFile with '$new'. Triggering SYSTEM apply task..."
|
||||
& schtasks.exe /run /tn $applyTaskName 2>&1 | ForEach-Object { Write-Host " schtasks: $_" }
|
||||
|
||||
# --- Wait for result ---
|
||||
$deadline = (Get-Date).AddSeconds(60)
|
||||
$result = $null
|
||||
while ((Get-Date) -lt $deadline) {
|
||||
if (Test-Path -LiteralPath $resultFile) {
|
||||
try {
|
||||
$result = Get-Content -LiteralPath $resultFile -Raw -ErrorAction Stop | ConvertFrom-Json
|
||||
break
|
||||
} catch { Start-Sleep -Milliseconds 200 }
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
|
||||
# Make the result MessageBox topmost so it shows above the FormTracePak /
|
||||
# DNC windows and isn't missed.
|
||||
$tmpForm = New-Object System.Windows.Forms.Form
|
||||
$tmpForm.TopMost = $true
|
||||
$tmpForm.WindowState = 'Minimized'
|
||||
$tmpForm.ShowInTaskbar = $false
|
||||
$tmpForm.Opacity = 0
|
||||
$tmpForm.Show()
|
||||
|
||||
if (-not $result) {
|
||||
Write-Host "Timed out waiting for SYSTEM apply task to produce result file ($resultFile)."
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
"Timed out waiting for the SYSTEM update task to complete.`n`nCheck:`n C:\Logs\SFLD\Apply-MachineNumber.log`n C:\Logs\SFLD\Prompt-MachineNumber.log`n`nThe prompt will appear again at next logon.",
|
||||
"Machine Number Update Timed Out",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
[System.Windows.Forms.MessageBoxIcon]::Warning
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Build summary from result JSON
|
||||
$lines = @()
|
||||
$lines += "Requested: $($result.Requested)"
|
||||
$lines += ""
|
||||
if ($result.UdcUpdated) { $lines += "UDC updated to $($result.Requested)" } else { $lines += "UDC: not updated (UDC may not be installed)" }
|
||||
if ($result.EdncUpdated) { $lines += "eDNC updated to $($result.Requested)" } else { $lines += "eDNC: not updated" }
|
||||
if ($result.UdcSettingsRestored) { $lines += "UDC settings restored from SFLD" }
|
||||
if ($result.UdcRestored) { $lines += "UDC live data restored from SFLD" }
|
||||
if ($result.MachineNumberTxtUpdated) { $lines += "machine-number.txt updated" }
|
||||
if ($result.MTConnectUpdated -and $result.MTConnectUpdated.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "MTConnect Devices.xml updates:"
|
||||
$result.MTConnectUpdated | ForEach-Object { $lines += " - $_" }
|
||||
}
|
||||
if ($result.Errors -and $result.Errors.Count -gt 0) {
|
||||
$lines += ""
|
||||
$lines += "FAILURES:"
|
||||
$result.Errors | ForEach-Object { $lines += " - $_" }
|
||||
}
|
||||
$lines += ""
|
||||
$lines += "Status: $($result.Status)"
|
||||
$lines += "Logs: C:\Logs\SFLD\Apply-MachineNumber.log"
|
||||
$lines += " C:\Logs\SFLD\Prompt-MachineNumber.log"
|
||||
$lines += ""
|
||||
$lines += "To apply eDNC changes, restart any running DncMain.exe."
|
||||
$summary = $lines -join "`n"
|
||||
|
||||
$icon = if ($result.Status -eq 'OK') { [System.Windows.Forms.MessageBoxIcon]::Information } else { [System.Windows.Forms.MessageBoxIcon]::Warning }
|
||||
[System.Windows.Forms.MessageBox]::Show(
|
||||
$tmpForm,
|
||||
$summary,
|
||||
"Machine Number Update Result",
|
||||
[System.Windows.Forms.MessageBoxButtons]::OK,
|
||||
$icon
|
||||
) | Out-Null
|
||||
$tmpForm.Close()
|
||||
|
||||
# Clean up result file for the next round.
|
||||
Remove-Item -LiteralPath $resultFile -Force -ErrorAction SilentlyContinue
|
||||
|
||||
# Only unregister the Prompt task on full success (no errors AND eDNC
|
||||
# updated to the requested value). If anything failed, leave it registered
|
||||
# for next logon retry.
|
||||
if ($result.Status -eq 'OK' -and $result.EdncUpdated) {
|
||||
Write-Host "All updates succeeded. Unregistering Prompt task."
|
||||
try { Unregister-ScheduledTask -TaskName $taskName -Confirm:$false -ErrorAction SilentlyContinue } catch {}
|
||||
} else {
|
||||
Write-Host "Some updates failed or skipped. Prompt task stays registered for next logon retry."
|
||||
}
|
||||
|
||||
Write-Host "Prompt-MachineNumber.ps1 finished $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||
try { Stop-Transcript | Out-Null } catch {}
|
||||
exit 0
|
||||
@@ -1,149 +0,0 @@
|
||||
# Register-CheckMachineNumberTask.ps1 - Register the two-task machine
|
||||
# number flow at imaging time:
|
||||
#
|
||||
# 1. "Prompt Machine Number" - AtLogOn, BUILTIN\Users, Limited.
|
||||
# Shows InputBox + writes new number to a request file, then triggers
|
||||
# the SYSTEM task via schtasks /run.
|
||||
#
|
||||
# 2. "Apply Machine Number" - on-demand only (no trigger), SYSTEM,
|
||||
# RunLevel Highest. Reads the request file, calls Update-MachineNumber
|
||||
# with full HKLM + ProgramData access, writes a result JSON, removes
|
||||
# the request file. No GUI - the Prompt task polls the result file
|
||||
# and displays the dialog.
|
||||
#
|
||||
# Replaces the old single-task design that ran as the logged-in user with
|
||||
# pre-granted BUILTIN\Users HKLM ACLs (02-MachineNumberACLs.ps1). That
|
||||
# approach was fragile (timing race with eDNC install, silent ACL skip)
|
||||
# and a security hole (any user could write to the machine-identity reg
|
||||
# key). With SYSTEM doing the actual writes, no ACL grants needed.
|
||||
#
|
||||
# Idempotent: safe to re-run. Existing tasks are overwritten.
|
||||
#
|
||||
# File kept named Register-CheckMachineNumberTask.ps1 (rather than
|
||||
# Register-MachineNumberTasks.ps1) so Run-ShopfloorSetup's existing
|
||||
# discovery doesn't need editing.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
||||
$logFile = Join-Path $logDir 'register-checkmn.log'
|
||||
|
||||
function Write-RegLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [INFO] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask start ==='
|
||||
|
||||
$promptTaskName = 'Prompt Machine Number'
|
||||
$applyTaskName = 'Apply Machine Number'
|
||||
$oldTaskName = 'Check Machine Number' # legacy, removed below
|
||||
|
||||
# Clean up the legacy single-task name from prior imaging cycles.
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $oldTaskName -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $oldTaskName -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Unregistered legacy task '$oldTaskName'"
|
||||
}
|
||||
} catch { Write-RegLog "Could not unregister legacy '$oldTaskName': $_" }
|
||||
|
||||
# Only arm the tasks if the bay was imaged with the 9999 placeholder. If
|
||||
# the tech entered a real machine number during PXE imaging it's already
|
||||
# in C:\Enrollment\machine-number.txt; no prompt needed on first logon.
|
||||
$mnFile = 'C:\Enrollment\machine-number.txt'
|
||||
$mnAtImaging = '9999'
|
||||
if (Test-Path -LiteralPath $mnFile) {
|
||||
$raw = (Get-Content -LiteralPath $mnFile -First 1 -ErrorAction SilentlyContinue)
|
||||
if ($raw) { $mnAtImaging = $raw.Trim() }
|
||||
}
|
||||
Write-RegLog "Imaging-time machine-number.txt = '$mnAtImaging'"
|
||||
if ($mnAtImaging -ne '9999') {
|
||||
Write-RegLog "Machine number is real ('$mnAtImaging' != 9999). Not registering tasks."
|
||||
foreach ($t in @($promptTaskName, $applyTaskName)) {
|
||||
try {
|
||||
if (Get-ScheduledTask -TaskName $t -ErrorAction SilentlyContinue) {
|
||||
Unregister-ScheduledTask -TaskName $t -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Unregistered stale task '$t'"
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask end (no-op) ==='
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Resolve script paths. Prefer the staged shopfloor-setup tree on C:
|
||||
# (where Run-ShopfloorSetup ran from); fall back to the same dir as this
|
||||
# Register script if invoked standalone.
|
||||
function Resolve-Script {
|
||||
param([string]$LeafName)
|
||||
$p = Join-Path $PSScriptRoot $LeafName
|
||||
if (Test-Path -LiteralPath $p) { return $p }
|
||||
$p = "C:\Enrollment\shopfloor-setup\Shopfloor\$LeafName"
|
||||
if (Test-Path -LiteralPath $p) { return $p }
|
||||
return $null
|
||||
}
|
||||
|
||||
$promptScript = Resolve-Script 'Prompt-MachineNumber.ps1'
|
||||
$applyScript = Resolve-Script 'Apply-MachineNumber.ps1'
|
||||
if (-not $promptScript) { Write-RegLog "Prompt-MachineNumber.ps1 not found - cannot register"; exit 1 }
|
||||
if (-not $applyScript) { Write-RegLog "Apply-MachineNumber.ps1 not found - cannot register"; exit 1 }
|
||||
Write-RegLog "Prompt script: $promptScript"
|
||||
Write-RegLog "Apply script: $applyScript"
|
||||
|
||||
# --- Prompt task (user-context, GUI) ---
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Normal -File `"$promptScript`""
|
||||
$trigger = New-ScheduledTaskTrigger -AtLogOn
|
||||
# Group SID S-1-5-32-545 = BUILTIN\Users (catches ShopFloor + support/admin
|
||||
# users that log in interactively). RunLevel Limited - no elevation; the
|
||||
# actual writes happen in the SYSTEM Apply task below.
|
||||
$principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Limited
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
|
||||
Register-ScheduledTask -TaskName $promptTaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null
|
||||
Write-RegLog "Registered scheduled task '$promptTaskName' (AtLogOn, BUILTIN\Users, Limited)"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register '$promptTaskName': $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# --- Apply task (SYSTEM, on-demand) ---
|
||||
try {
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$applyScript`""
|
||||
# No trigger - the Prompt task starts this via schtasks /run /tn.
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 10)
|
||||
Register-ScheduledTask -TaskName $applyTaskName -Action $action -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null
|
||||
Write-RegLog "Registered scheduled task '$applyTaskName' (on-demand, SYSTEM, Highest)"
|
||||
|
||||
# Default SDDL on a SYSTEM-owned task only grants Admins + SYSTEM
|
||||
# FullAccess - BUILTIN\Users can't see or run it via schtasks /run.
|
||||
# Add an ACE granting BUILTIN\Users GenericRead + GenericExecute so the
|
||||
# user-context Prompt task can trigger this Apply task on demand. They
|
||||
# still can't modify/delete it - only read+execute.
|
||||
try {
|
||||
$svc = New-Object -ComObject Schedule.Service
|
||||
$svc.Connect()
|
||||
$taskObj = $svc.GetFolder('\').GetTask($applyTaskName)
|
||||
# GenericRead = 0x80000000 (GR), GenericExecute = 0x20000000 (GX)
|
||||
# BU = BUILTIN\Users
|
||||
$newSd = 'O:BAG:BAD:(A;;FA;;;BA)(A;;FA;;;SY)(A;;GRGX;;;BU)'
|
||||
# SetSecurityDescriptor flag 0 = default, persists DACL change.
|
||||
$taskObj.SetSecurityDescriptor($newSd, 0)
|
||||
Write-RegLog "Granted BUILTIN\Users GR+GX on '$applyTaskName' (so Limited users can schtasks /run)"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to set task SDDL on '$applyTaskName': $_ (Limited users may not be able to trigger Apply)"
|
||||
}
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register '$applyTaskName': $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-CheckMachineNumberTask end ==='
|
||||
exit 0
|
||||
@@ -1,79 +0,0 @@
|
||||
# Register-MapSfldShare.ps1 - Stage Map-SfldShare.ps1 + register an
|
||||
# HKLM\Run entry that maps S: for any interactive user (SupportUser,
|
||||
# ShopFloor, any future end-user accounts).
|
||||
#
|
||||
# Why HKLM\Run instead of a scheduled task: Run fires at Explorer
|
||||
# startup in the logged-in user's interactive session with their full
|
||||
# token + HKCU mounted. No principal/LogonType/group-SID plumbing, no
|
||||
# "task fires in session 0 but drive not visible to Explorer" class of
|
||||
# bugs. Works for every BUILTIN\Users member with no extra logic.
|
||||
#
|
||||
# Why not the vendor's ConsumeCredentials.ps1: it calls
|
||||
# New-StoredCredential -Persist LocalMachine (needs admin) before net
|
||||
# use. ShopFloor is non-admin, so the cred-store fails and net use has
|
||||
# no auth. Our Map-SfldShare.ps1 reads HKLM creds directly and passes
|
||||
# them inline to net use /user: -- no Credential Manager needed.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$installRoot = 'C:\Program Files\GE\SfldShare'
|
||||
$mapScript = Join-Path $installRoot 'Map-SfldShare.ps1'
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
$logFile = Join-Path $logDir 'register-mapshare.log'
|
||||
$runKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run'
|
||||
$runValue = 'GE Map SFLD Share'
|
||||
$legacyTask = 'GE Shopfloor Map S: Drive'
|
||||
|
||||
if (-not (Test-Path $logDir)) { New-Item -Path $logDir -ItemType Directory -Force | Out-Null }
|
||||
|
||||
function Write-RegLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [INFO] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare start ==='
|
||||
|
||||
# Stage our Map-SfldShare.ps1 to a persistent location
|
||||
if (-not (Test-Path $installRoot)) {
|
||||
New-Item -Path $installRoot -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
$src = Join-Path $PSScriptRoot 'lib\Map-SfldShare.ps1'
|
||||
if (Test-Path $src) {
|
||||
Copy-Item -Path $src -Destination $mapScript -Force
|
||||
Write-RegLog "Staged $src -> $mapScript"
|
||||
} else {
|
||||
Write-RegLog "Map-SfldShare.ps1 not found at $src - cannot register"
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Remove the legacy scheduled task if it exists (left behind by older
|
||||
# imaging runs that used the scheduled-task approach).
|
||||
if (Get-ScheduledTask -TaskName $legacyTask -ErrorAction SilentlyContinue) {
|
||||
try {
|
||||
Unregister-ScheduledTask -TaskName $legacyTask -Confirm:$false -ErrorAction Stop
|
||||
Write-RegLog "Removed legacy scheduled task '$legacyTask'"
|
||||
} catch {
|
||||
Write-RegLog "Failed to remove legacy task '$legacyTask': $_"
|
||||
}
|
||||
}
|
||||
|
||||
# Register HKLM\Run entry. Runs at Explorer startup for every
|
||||
# interactive user in that user's session.
|
||||
try {
|
||||
$command = '"{0}" -NoProfile -ExecutionPolicy Bypass -File "{1}"' -f `
|
||||
"$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe", $mapScript
|
||||
|
||||
if (-not (Test-Path $runKey)) {
|
||||
New-Item -Path $runKey -Force | Out-Null
|
||||
}
|
||||
New-ItemProperty -Path $runKey -Name $runValue -Value $command -PropertyType String -Force | Out-Null
|
||||
Write-RegLog "Set $runKey\$runValue = $command"
|
||||
} catch {
|
||||
Write-RegLog "FAILED to register Run key: $_"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-RegLog '=== Register-MapSfldShare end ==='
|
||||
exit 0
|
||||
32
playbook/shopfloor-setup/Shopfloor/Register-PostPpkgTask.ps1
Normal file
32
playbook/shopfloor-setup/Shopfloor/Register-PostPpkgTask.ps1
Normal file
@@ -0,0 +1,32 @@
|
||||
# Register-PostPpkgTask.ps1 - register the "GE Shopfloor Post-PPKG" task.
|
||||
#
|
||||
# Called by Run-ShopfloorSetup.ps1 BEFORE the PPKG step (the PPKG reboot can
|
||||
# kill the caller). Copies the runner out of C:\Enrollment (the bulk-enroll
|
||||
# cleanup may remove staging folders) to C:\Program Files\GE\Shopfloor and
|
||||
# registers a SYSTEM task that runs Invoke-PostPpkg.ps1 at every startup and
|
||||
# logon until it has nothing left to do; then it unregisters itself.
|
||||
#
|
||||
# Idempotent: re-registering replaces the task.
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$runtimeDir = 'C:\Program Files\GE\Shopfloor'
|
||||
$taskName = 'GE Shopfloor Post-PPKG'
|
||||
|
||||
New-Item -Path $runtimeDir -ItemType Directory -Force | Out-Null
|
||||
foreach ($f in @('Install-AppList.ps1', 'Invoke-PostPpkg.ps1')) {
|
||||
Copy-Item -LiteralPath (Join-Path $PSScriptRoot $f) -Destination (Join-Path $runtimeDir $f) -Force
|
||||
}
|
||||
|
||||
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
|
||||
-Argument "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$runtimeDir\Invoke-PostPpkg.ps1`""
|
||||
$triggers = @(
|
||||
(New-ScheduledTaskTrigger -AtStartup),
|
||||
(New-ScheduledTaskTrigger -AtLogOn)
|
||||
)
|
||||
$principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -LogonType ServiceAccount -RunLevel Highest
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
|
||||
-StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Hours 2)
|
||||
|
||||
Register-ScheduledTask -TaskName $taskName -Action $action -Trigger $triggers `
|
||||
-Principal $principal -Settings $settings -Force | Out-Null
|
||||
Write-Host "Registered '$taskName' (SYSTEM, at startup and logon): $runtimeDir\Invoke-PostPpkg.ps1"
|
||||
@@ -10,3 +10,8 @@ if %errorlevel% neq 0 (
|
||||
|
||||
|
||||
"C:\Program Files\PowerShell\7\pwsh.exe" -NoProfile -ExecutionPolicy Bypass -File "C:\Program Files\Sysinternals\sfld_autologon.ps1"
|
||||
|
||||
REM 2) Clear any leftover imaging AutoLogonCount. Imaging tops it up for
|
||||
REM SupportUser and Autologon leaves it in place, so ShopFloor autologon
|
||||
REM would switch itself off again when it counts down to 0.
|
||||
reg delete "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoLogonCount /f /reg:64 >nul 2>&1
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
'----------------------------------------------------------------------
|
||||
' This macro was created by the macro recorder.
|
||||
' Macro File: Office.ebs
|
||||
' Date: Wed May 18 09:55:44 2016
|
||||
' Recorded for profile: WJ_Office
|
||||
'----------------------------------------------------------------------
|
||||
|
||||
Sub Main
|
||||
Dim HostExplorer as Object
|
||||
Dim MyHost as Object
|
||||
Dim Rc as Integer
|
||||
|
||||
Dim iPSUpdateTimeout
|
||||
Dim iWaitForStringTimeout
|
||||
|
||||
On Error goto GenericErrorHandler
|
||||
|
||||
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||
Set MyHost = HostExplorer.HostFromProfile("WJ_Office") ' Set object for the desired session
|
||||
If MyHost is Nothing Then Goto NoSession
|
||||
|
||||
iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
|
||||
iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
|
||||
|
||||
Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Rc = MyHost.Keys("shop_pc^M")
|
||||
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||
Rc = MyHost.WaitForString( "Do you have a barcode reader?", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Exit Sub
|
||||
|
||||
'-------------------- Runtime Error Handlers --------------------
|
||||
GenericErrorHandler:
|
||||
Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
NoSession:
|
||||
Msgbox "Profile ""WJ_Office"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitPSUpdatedTimeout:
|
||||
Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitForStringTimeout:
|
||||
Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
End Sub
|
||||
@@ -1,35 +0,0 @@
|
||||
'----------------------------------------------------------------------
|
||||
' This macro was created by the macro recorder.
|
||||
' Macro File: mmcs.ebs
|
||||
' Date: Tue Jun 04 08:56:51 2013
|
||||
' Recorded for profile: mmcs
|
||||
'----------------------------------------------------------------------
|
||||
|
||||
Sub Main
|
||||
Dim HostExplorer as Object
|
||||
Dim MyHost as Object
|
||||
Dim iIdleTime
|
||||
|
||||
Dim iPSUpdateTime
|
||||
|
||||
On Error goto ErrorCheck
|
||||
|
||||
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||
Set MyHost = HostExplorer.HostFromProfile("mmcs") ' Set object for the desired session
|
||||
|
||||
iPSUpdateTime = 60 ' PS Update wait time set to 60 seconds
|
||||
iIdleTime = 2000 ' Idle time set to 2000 milliseconds
|
||||
|
||||
MyHost.WaitPSUpdated(iPSUpdateTime)
|
||||
Rc = MyHost.WaitForString("Username:", -1, 9999, TRUE)
|
||||
MyHost.Keys("mmcswj^M")
|
||||
MyHost.WaitPSUpdated(iPSUpdateTime)
|
||||
Rc = MyHost.WaitForString("| Badge : |", -1, 9999, TRUE)
|
||||
Exit Sub
|
||||
|
||||
ErrorCheck:
|
||||
if (Err = 440) Then
|
||||
Msgbox "The specified session is not running.", 16, "Hummingbird Macro Error"
|
||||
End If
|
||||
Exit Sub
|
||||
End Sub
|
||||
@@ -1,56 +0,0 @@
|
||||
'----------------------------------------------------------------------
|
||||
' This macro was created by the macro recorder.
|
||||
' Macro File: shopfloor.ebs
|
||||
' Date: Wed May 18 09:57:00 2016
|
||||
' Recorded for profile: WJ Shopfloor
|
||||
'----------------------------------------------------------------------
|
||||
|
||||
Sub Main
|
||||
Dim HostExplorer as Object
|
||||
Dim MyHost as Object
|
||||
Dim Rc as Integer
|
||||
|
||||
Dim iPSUpdateTimeout
|
||||
Dim iWaitForStringTimeout
|
||||
|
||||
On Error goto GenericErrorHandler
|
||||
|
||||
Set HostExplorer = CreateObject("HostExplorer") ' Initialize HostExplorer Object
|
||||
Set MyHost = HostExplorer.HostFromProfile("WJ Shopfloor") ' Set object for the desired session
|
||||
If MyHost is Nothing Then Goto NoSession
|
||||
|
||||
iPSUpdateTimeout = 60 ' WaitPSUpdated timeout set to 60 seconds
|
||||
iWaitForStringTimeout = 60 ' WaitForString timeout set to 60 seconds
|
||||
|
||||
Rc = MyHost.WaitForString( "Username:", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Rc = MyHost.Keys("shop_xmi^M")
|
||||
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||
Rc = MyHost.WaitForString( "Password:", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Rc = MyHost.Keys("dnc123^M")
|
||||
Rc = MyHost.WaitPSUpdated( iPSUpdateTimeout, TRUE )
|
||||
If Rc <> 0 Then Goto OnWaitPSUpdatedTimeout
|
||||
Rc = MyHost.WaitForString( "| EXIT | |", -1, iWaitForStringTimeout, TRUE )
|
||||
If Rc = 0 Then Goto OnWaitForStringTimeout
|
||||
Exit Sub
|
||||
|
||||
'-------------------- Runtime Error Handlers --------------------
|
||||
GenericErrorHandler:
|
||||
Msgbox "Error " & Err & " : """ & Error(Err) & """ has occurred on line " & Erl-1 & "." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
NoSession:
|
||||
Msgbox "Profile ""WJ Shopfloor"" is not running." & Chr(10) & "Unable to execute macro.", 16, "HostExplorer Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitPSUpdatedTimeout:
|
||||
Msgbox "Timeout occured waiting for host to update screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
OnWaitForStringTimeout:
|
||||
Msgbox "Timeout occured waiting for string on host screen." & Chr(10) & "Unable to continue macro execution.", 16, "HostExplorer Basic Macro Error"
|
||||
Exit Sub
|
||||
|
||||
End Sub
|
||||
@@ -1,9 +0,0 @@
|
||||
[KEYMAP]
|
||||
Normal Entries=1
|
||||
Normal0=Backspace,Delete
|
||||
Shift Entries=0
|
||||
Ctrl Entries=0
|
||||
ShiftCtrl Entries=0
|
||||
Alt Entries=0
|
||||
ShiftAlt Entries=0
|
||||
AltCtrl Entries=0
|
||||
@@ -1,13 +0,0 @@
|
||||
[KEYMAP]
|
||||
Normal Entries=5
|
||||
Normal0=F4,Pf4
|
||||
Normal1=Subtract,Num-Pad-Minus
|
||||
Normal2=Multiply,Num-Pad-*
|
||||
Normal3=Divide,Num-Pad-/
|
||||
Normal4=Backspace,Delete
|
||||
Shift Entries=0
|
||||
Ctrl Entries=0
|
||||
ShiftCtrl Entries=0
|
||||
Alt Entries=0
|
||||
ShiftAlt Entries=0
|
||||
AltCtrl Entries=0
|
||||
@@ -1,23 +0,0 @@
|
||||
[Version]
|
||||
AssemblyVersion=1
|
||||
[Console]
|
||||
Title=HostExplorer Menu Editor
|
||||
IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
|
||||
IconID=IDI_CSS_CONSOLE
|
||||
[Attributes]
|
||||
SupportAssemblyEditing=1
|
||||
SupportSeparator=1
|
||||
ParentLevels=100
|
||||
[Parent Node]
|
||||
name=Menu
|
||||
[SubParent Node]
|
||||
name=Submenu
|
||||
[Child Node]
|
||||
name=Menu Option
|
||||
[Messages]
|
||||
Create New Parent=Create New Menu
|
||||
Create New SubParent=Create New Submenu
|
||||
Insert New Parent=Insert New Menu
|
||||
Insert New SubParent=Insert New Submenu
|
||||
[Root Group]
|
||||
Name=Menu
|
||||
@@ -1,474 +0,0 @@
|
||||
[version]
|
||||
AssemblyVersion=1
|
||||
|
||||
[nls]
|
||||
filename=menunls$LANGUAGE_EXTENSION$.hma
|
||||
|
||||
[Console]
|
||||
Title=HostExplorer Menu Editor
|
||||
IconPath=HumCSSPlugins.HumCSSActiveTunnelsPlugin
|
||||
IconID=IDI_CSS_CONSOLE
|
||||
|
||||
[Attributes]
|
||||
SupportAssemblyEditing=1
|
||||
SupportEnablingDisablingItems=0
|
||||
SupportSeparator=1
|
||||
|
||||
[Parent Node]
|
||||
Nameid=Parent
|
||||
|
||||
[SubParent Node]
|
||||
Nameid=SubParent
|
||||
|
||||
[Child Node]
|
||||
Nameid=Child
|
||||
|
||||
[Messages]
|
||||
Nameid1=Create New Parent
|
||||
Nameid2=Create New SubParent
|
||||
Nameid3=Insert New Parent
|
||||
Nameid4=Insert New SubParent
|
||||
|
||||
[root group]
|
||||
Name=Menu
|
||||
item1=File
|
||||
item2=Edit
|
||||
item3=Transfer
|
||||
item4=Fonts
|
||||
item5=options
|
||||
item6=tools
|
||||
item7=view
|
||||
item8=window
|
||||
item9=help
|
||||
|
||||
[Separator]
|
||||
name=
|
||||
id=0
|
||||
|
||||
[File]
|
||||
Nameid=File
|
||||
id=4071
|
||||
Parent=1
|
||||
item1=new session
|
||||
item2=duplicate session
|
||||
item3=open session
|
||||
item4=open session in same window
|
||||
item5=save session profile
|
||||
item6=close session
|
||||
item7=separator
|
||||
item8=open layout
|
||||
item9=save layout
|
||||
item10=separator
|
||||
item11=connect
|
||||
item12=disconnect
|
||||
item13=separator
|
||||
item14=print screen
|
||||
item15=print multiple screens
|
||||
item16=save screen
|
||||
item17=send screen
|
||||
item18=separator
|
||||
item19=screen capture
|
||||
item20=separator
|
||||
item21=Recent Sessions Submenu
|
||||
item22=separator
|
||||
item23=exit all
|
||||
|
||||
[New Session]
|
||||
nameid=New Session
|
||||
id=Dlg-New-Session
|
||||
|
||||
[Duplicate Session]
|
||||
nameid=Duplicate Session
|
||||
id=Duplicate-Session
|
||||
|
||||
[Open Session]
|
||||
nameid=Open Session
|
||||
id=Dlg-Open-Session
|
||||
|
||||
[Open Session In Same Window]
|
||||
nameid=Open Session In Same Window
|
||||
id=Dlg-Open-Session-In-Same-Window
|
||||
|
||||
[Save Session Profile]
|
||||
nameid=Save Session Profile
|
||||
id=Dlg-Save-Profile
|
||||
|
||||
[Close Session]
|
||||
nameid=Close Session
|
||||
id=Dlg-Close-Session
|
||||
|
||||
[open layout]
|
||||
nameid=Open Layout
|
||||
id=Dlg-Open-Layout
|
||||
|
||||
[save layout]
|
||||
nameid=Save Layout
|
||||
id=Dlg-Save-Layout
|
||||
|
||||
[connect]
|
||||
nameid=connect
|
||||
id=Connect
|
||||
|
||||
[disconnect]
|
||||
nameid=disconnect
|
||||
id=Disconnect
|
||||
|
||||
[print screen]
|
||||
nameid=print screen
|
||||
id=Dlg-Print-Screen
|
||||
|
||||
[print multiple screens]
|
||||
nameid=print multiple screens
|
||||
id=Dlg-Print-Multiple-Screens
|
||||
|
||||
|
||||
[save screen]
|
||||
nameid=Save Screen
|
||||
id=Dlg-Save-Screen
|
||||
|
||||
[send screen]
|
||||
nameid=Send Screen
|
||||
id=Send-Screen
|
||||
|
||||
[screen capture]
|
||||
nameid=Screen Capture
|
||||
id=Toggle-Capture
|
||||
|
||||
[Recent Sessions Submenu]
|
||||
nameid=Recent Sessions
|
||||
id=4187
|
||||
Parent=1
|
||||
item1=recent sessions
|
||||
|
||||
[recent sessions]
|
||||
nameid=Recent Sessions
|
||||
id=Recent-Sessions
|
||||
|
||||
[Exit all]
|
||||
nameid=exit all
|
||||
id=Dlg-Exit
|
||||
|
||||
|
||||
[Edit]
|
||||
Nameid=Edit
|
||||
id=4072
|
||||
Parent=1
|
||||
item1=copy
|
||||
item2=copy append
|
||||
item3=paste
|
||||
item4=separator
|
||||
item5=select all
|
||||
item6=separator
|
||||
item7=edit find
|
||||
item8=separator
|
||||
item9=clear display
|
||||
item10=clear all
|
||||
item11=soft terminal reset
|
||||
|
||||
[undo]
|
||||
nameid=undo
|
||||
id=Edit-Undo
|
||||
|
||||
[redo]
|
||||
nameid=redo
|
||||
id=Edit-Redo
|
||||
|
||||
[cut]
|
||||
nameid=cut
|
||||
id=Edit-Cut
|
||||
|
||||
[Copy]
|
||||
nameid=copy
|
||||
id=Edit-Copy
|
||||
|
||||
[Copy append]
|
||||
nameid=Copy Append
|
||||
id=Edit-Copy-Append
|
||||
|
||||
[Paste]
|
||||
nameid=Paste
|
||||
id=Edit-Paste
|
||||
|
||||
[Paste continue]
|
||||
nameid=Paste Continue
|
||||
id=Edit-Paste-Continue
|
||||
|
||||
[Select All]
|
||||
nameid=Select All
|
||||
id=Edit-SelectAll
|
||||
|
||||
[Edit Options]
|
||||
nameid=Options
|
||||
id=Dlg-Options
|
||||
|
||||
[edit find]
|
||||
nameid=Find
|
||||
id=Dlg-Find
|
||||
|
||||
[clear display]
|
||||
nameid=Clear Display
|
||||
id=Clear-Display
|
||||
|
||||
[clear all]
|
||||
nameid=Clear All
|
||||
id=Clear-Buffer
|
||||
|
||||
[soft terminal reset]
|
||||
nameid=Soft Terminal Reset
|
||||
id=Power-On-Reset
|
||||
|
||||
|
||||
[Transfer]
|
||||
Nameid=Transfer
|
||||
id=4073
|
||||
Parent=1
|
||||
item1=send
|
||||
item2=receive
|
||||
|
||||
[send]
|
||||
nameid=Send File to Host
|
||||
id=Dlg-Upload
|
||||
|
||||
[receive]
|
||||
nameid=Receive File from Host
|
||||
id=Dlg-Download
|
||||
|
||||
|
||||
[Fonts]
|
||||
Nameid=Fonts
|
||||
id=4074
|
||||
Parent=1
|
||||
item1=next larger
|
||||
item2=next smaller
|
||||
item3=choose font
|
||||
item4=maximize font
|
||||
|
||||
[next larger]
|
||||
nameid=next larger font
|
||||
id=Font-Larger
|
||||
|
||||
[next smaller]
|
||||
nameid=Next smaller font
|
||||
id=Font-Smaller
|
||||
|
||||
[choose font]
|
||||
nameid=font
|
||||
id=Dlg-Font-Select
|
||||
|
||||
[maximize font]
|
||||
nameid=Maximize font
|
||||
id=Maximize-Font
|
||||
|
||||
|
||||
[options]
|
||||
nameid=Options
|
||||
id=4075
|
||||
Parent=1
|
||||
item1=global options
|
||||
item2=api settings
|
||||
item3=separator
|
||||
item4=keyboard mapping
|
||||
item5=quick keys
|
||||
item6=separator
|
||||
item7=session properties
|
||||
|
||||
[global options]
|
||||
nameid=Global Options
|
||||
id=Dlg-Global
|
||||
|
||||
[api settings]
|
||||
nameid=API Settings
|
||||
id=Dlg-API-Settings
|
||||
|
||||
[keyboard mapping]
|
||||
nameid=Keyboard Mapping
|
||||
id=Dlg-Keyboard-Mapper
|
||||
|
||||
[quick keys]
|
||||
nameid=Quick Keys
|
||||
id=Dlg-Quick-Key-Editor
|
||||
|
||||
[session properties]
|
||||
nameid=session Properties
|
||||
id=Dlg-Edit-Session-Profile
|
||||
|
||||
|
||||
[tools]
|
||||
nameid=Tools
|
||||
id=4076
|
||||
Parent=1
|
||||
item1=Macro Submenu
|
||||
item2=Quick Script Submenu
|
||||
item3=separator
|
||||
item4=Customizetoolbar
|
||||
item5=Customizemenu
|
||||
item6=Customizesessionproperties
|
||||
|
||||
[Macro Submenu]
|
||||
nameid=Macro
|
||||
id=4177
|
||||
Parent=1
|
||||
item1=edit macro
|
||||
item2=run macro
|
||||
item3=separator
|
||||
item4=start recording
|
||||
item5=pause recording
|
||||
item6=resume recording
|
||||
item7=stop recording
|
||||
item8=cancel recording
|
||||
|
||||
[Edit macro]
|
||||
nameid=Macro Edit
|
||||
id=Dlg-Edit-Macro
|
||||
|
||||
[run macro]
|
||||
nameid=run
|
||||
id=Dlg-Run-Macro
|
||||
|
||||
[start recording]
|
||||
nameid=Start Recording
|
||||
id=Record-Macro
|
||||
|
||||
[pause recording]
|
||||
nameid=Pause Recording
|
||||
id=Toggle-Recording-Pause
|
||||
|
||||
[resume recording]
|
||||
nameid=Resume Recording
|
||||
id=Resume-Recording-Macro
|
||||
|
||||
[stop recording]
|
||||
nameid=Stop Recording
|
||||
id=End-Recording
|
||||
|
||||
[cancel recording]
|
||||
nameid=Cancel Recording
|
||||
id=Cancel-Macro-Recording
|
||||
|
||||
[Quick Script Submenu]
|
||||
nameid=Quick Script
|
||||
id=4091
|
||||
Parent=1
|
||||
item1=edit qs
|
||||
item2=run qs
|
||||
item3=stop qs
|
||||
item4=separator
|
||||
item5=start recording qs
|
||||
item6=pause recording qs
|
||||
item7=resume recording qs
|
||||
item8=stop recording qs
|
||||
item9=cancel recording qs
|
||||
|
||||
[Customizetoolbar]
|
||||
nameid=Customize Toolbar
|
||||
id=Dlg-Toolbars
|
||||
|
||||
[Customizemenu]
|
||||
nameid=Customize Menu
|
||||
id=Dlg-Customize-Menus
|
||||
|
||||
[CustomizeSessionProperties]
|
||||
nameid=Customize Session Properties
|
||||
id=Dlg-Customize-Session-Properties
|
||||
|
||||
[Edit qs]
|
||||
nameid=Edit
|
||||
id=Dlg-QuickScript-Editor
|
||||
|
||||
[run qs]
|
||||
nameid=Run
|
||||
id=Dlg-Run-QuickScript
|
||||
|
||||
[stop qs]
|
||||
nameid=Stop
|
||||
id=Stop-QuickScript
|
||||
|
||||
[start recording qs]
|
||||
nameid=Start Recording
|
||||
id=Record-QuickScript
|
||||
|
||||
[pause recording qs]
|
||||
nameid=Pause Recording
|
||||
id=Pause-Recording-QuickScript
|
||||
|
||||
[resume recording qs]
|
||||
nameid=Resume Recording
|
||||
id=Resume-Recording-QuickScript
|
||||
|
||||
[stop recording qs]
|
||||
nameid=Stop Recording
|
||||
id=Stop-Recording-QuickScript
|
||||
|
||||
[cancel recording qs]
|
||||
nameid=Cancel Recording
|
||||
id=Cancel-Recording-QuickScript
|
||||
|
||||
|
||||
[view]
|
||||
nameid=view
|
||||
id=4178
|
||||
Parent=1
|
||||
item1=hotspots
|
||||
item2=row and column indicator
|
||||
item3=cross hair cursor
|
||||
item4=full screen
|
||||
|
||||
[hotspots]
|
||||
nameid=Hotspots
|
||||
id=Toggle-View-Hotspots
|
||||
|
||||
[Row and Column Indicator]
|
||||
nameid=Row and Column Indicator
|
||||
id=Toggle-Row-And-Column-Indicator
|
||||
|
||||
[cross hair cursor]
|
||||
nameid=Cross-Hair Cursor
|
||||
id=Toggle-CrossHair-Cursor
|
||||
|
||||
[full screen]
|
||||
nameid=Full Screen
|
||||
id=Toggle-Full-Screen
|
||||
|
||||
|
||||
[window]
|
||||
nameid=Window
|
||||
id=4179
|
||||
Parent=1
|
||||
item1=cascade
|
||||
item2=next session
|
||||
item3=previous session
|
||||
item4=separator
|
||||
item5=session window list
|
||||
|
||||
[cascade]
|
||||
nameid=Cascade
|
||||
id=Cascade-Session-Windows
|
||||
|
||||
[next session]
|
||||
nameid=next session
|
||||
id=Next-Session
|
||||
|
||||
[previous session]
|
||||
nameid=previous session
|
||||
id=Prev-Session
|
||||
|
||||
[session window list]
|
||||
name=<List Of Windows>
|
||||
id=4142
|
||||
|
||||
|
||||
[help]
|
||||
nameid=Help
|
||||
id=4180
|
||||
Parent=1
|
||||
item1=contents
|
||||
item2=separator
|
||||
item3=about
|
||||
|
||||
[contents]
|
||||
nameid=Contents
|
||||
id=Help-Index
|
||||
|
||||
[about]
|
||||
nameid=about
|
||||
id=Help-About
|
||||
Binary file not shown.
@@ -1,106 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Properties=Default 3270
|
||||
Menu=Default 3270
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=ibmqks.ae.ge.com
|
||||
Session Keep Alive Interval=600
|
||||
PC Keyboard Type=5
|
||||
IND$FILE Parms=0,0,1,1,0,0,0,257,253,2048,200,430,430,0,1252,"IND$FILE","","","",0
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 70 405 822 595 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1936 1048 0 0 0=26 12 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
QuickKey=Default
|
||||
Never Run Profile=Off
|
||||
Profile File Version=200
|
||||
Space Allocation Parms=0,0,0,0
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Cryptographic Mode=1
|
||||
IP Port=992
|
||||
Security Option=2
|
||||
SSL/TLS User Cipher Suites=DEFAULT
|
||||
[HEBar-59440]
|
||||
sizeFloatCX=505
|
||||
sizeFloatCY=28
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1920
|
||||
ScreenCY=1080
|
||||
[Session Options]
|
||||
Last Selected Page=139
|
||||
[Toolbars]
|
||||
ToolBarVersion=6
|
||||
bar0=Default 3270 ToolBar.tb3
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=512
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-1
|
||||
MRUFloatYPos=1025
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=3
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
@@ -1,122 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Terminal Type=2
|
||||
Session Properties=Default VT
|
||||
Menu=Default VT
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||
Session Keep Alive Interval=600
|
||||
PC Keyboard Type=5
|
||||
Tabstops='8 16 24 32 40 48 56 64 72 '
|
||||
VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
|
||||
Default VT Recv Path=$MYDOCUMENTS$\
|
||||
SSL/TLS Version=3
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 176 21 892 619 0 0 0=-17 0 0 0 400 0 0 0 0 3 2 2 0 Courier New| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
QuickKey=Super Key F6
|
||||
Never Run Profile=Off
|
||||
dwVTFlags1=29369344
|
||||
VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
|
||||
Connect Timeout=60
|
||||
Disable Secure Connection Warning=Yes
|
||||
Profile File Version=200
|
||||
Cryptographic Mode=1
|
||||
Display RowCol Indicator=Off
|
||||
VT Scrollback Save Attribs=Off
|
||||
VT True Display Mode=1
|
||||
Status Line=1
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Keyboard=Office
|
||||
Auto Start Quick-Key=shopfloor.ebs
|
||||
On Disconnect=0
|
||||
Allow sleep while connected=0
|
||||
Toolbar Scheme Name=Default VT
|
||||
Save Profile on Window Close=Off
|
||||
Toolbar Scheme Modified=Default VT:30026745--1815298816
|
||||
Save Toolbar Info on Exit=Off
|
||||
Save Mode=0
|
||||
[Session Options]
|
||||
Last Selected Page=4000
|
||||
[HEBar-59440]
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1600
|
||||
ScreenCY=1200
|
||||
[Toolbars]
|
||||
ToolBarVersion=5
|
||||
bar0=Default VT ToolBar.tbv
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
Visible=0
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=541
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-2147483648
|
||||
MRUFloatYPos=0
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=3
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
@@ -1,122 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Terminal Type=2
|
||||
Session Properties=Default VT
|
||||
Menu=Default VT
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||
Session Keep Alive Interval=600
|
||||
PC Keyboard Type=5
|
||||
Tabstops='8 16 24 32 40 48 56 64 72 '
|
||||
VT BG Colors='1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 8 7 7 9 15 14 10 2 10 '
|
||||
Default VT Recv Path=$MYDOCUMENTS$\
|
||||
SSL/TLS Version=3
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 22 22 892 619 0 0 0=20 10 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1634 1066 0 0 0=30 15 0 0 700 0 0 0 1 3 2 2 0 HE_Bitmap| 0 0
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
QuickKey=Super Key F6
|
||||
Never Run Profile=Off
|
||||
dwVTFlags1=29369344
|
||||
VT FG Colors='7 7 9 15 14 10 2 10 0 1 2 3 4 5 15 7 8 9 10 11 12 13 14 15 15 0 0 1 0 0 0 0 0 0 0 '
|
||||
Connect Timeout=60
|
||||
Disable Secure Connection Warning=Yes
|
||||
Profile File Version=200
|
||||
Cryptographic Mode=1
|
||||
Display RowCol Indicator=Off
|
||||
VT Scrollback Save Attribs=Off
|
||||
VT True Display Mode=1
|
||||
Status Line=1
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Keyboard=Office
|
||||
Auto Start Quick-Key=Office.ebs
|
||||
On Disconnect=0
|
||||
Allow sleep while connected=0
|
||||
Toolbar Scheme Name=Default VT
|
||||
Save Profile on Window Close=Off
|
||||
Toolbar Scheme Modified=Default VT:30026745--1815298816
|
||||
Save Toolbar Info on Exit=Off
|
||||
Save Mode=0
|
||||
[Session Options]
|
||||
Last Selected Page=4000
|
||||
[HEBar-59440]
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1600
|
||||
ScreenCY=1200
|
||||
[Toolbars]
|
||||
ToolBarVersion=5
|
||||
bar0=Default VT ToolBar.tbv
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
Visible=0
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=541
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-2147483648
|
||||
MRUFloatYPos=0
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=3
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
@@ -1,128 +0,0 @@
|
||||
[PROFILE]
|
||||
Session Terminal Type=2
|
||||
Never Run Profile=Off
|
||||
Session Properties=Default VT
|
||||
Menu=Default VT
|
||||
Auto Run Macro Delay Time=1
|
||||
Gateway Name=wjfms3.apps.wlm.geaerospace.net
|
||||
Session Keep Alive Interval=600
|
||||
dwVTFlags1=29369344
|
||||
PC Keyboard Type=5
|
||||
Keyboard=ATM-VT420
|
||||
Tabstops='8 16 24 32 40 48 56 64 72 80 88 96 104 112 120 128 136 144 152 160 168 176 184 192 200 208 216 224 232 240 248 256 264 272 280 288 296 '
|
||||
Default VT Recv Path=$MYDOCUMENTS$\
|
||||
SSL/TLS Version=3
|
||||
SSL/TLS Current User Certificate=1
|
||||
Window Title=%s - %p (%h)
|
||||
WinInfo Default Restored=0 0 0 0 10 10 757 491 0 0 0=16 9 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
WinInfo Default Maximized=0 0 0 0 -8 -8 1382 744 0 0 0=26 13 0 0 400 0 0 0 0 3 2 2 0 HE_Bitmap| 0 0
|
||||
Window State=2
|
||||
Use Specific PRTSCR Printer=On
|
||||
PrtScr DevMode=1025,1024,156,1497,33565,1,256,2794,1240,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
|
||||
PrtScr Driver=winspool
|
||||
PrtScr Device=ZDesigner TLP 3842
|
||||
PrtScr Output=LPT1:
|
||||
Print Header2=Page: %# Document Name: %F
|
||||
Use Specific VT Printer=On
|
||||
VT DevMode=1025,1024,156,1497,323347,1,256,190,914,0,1,4,300,1,1,300,1,0,0,1,0,0,0,0,ZDesigner TLP 3842,User defined
|
||||
VT Driver=winspool
|
||||
VT Device=ZDesigner TLP 3842
|
||||
VT Output=USB001
|
||||
Printer Target=1
|
||||
VT Printer Bypass Windows Print Driver=Off
|
||||
VT Printer Use Default Font=On
|
||||
VT Printer Margins Left=320
|
||||
VT Printer Margins Right=0
|
||||
VT Printer Margins Bottom=0
|
||||
VT Printer Margins Top=0
|
||||
QuickKey=ID
|
||||
Auto Start Quick-Key=mmcs.ebs
|
||||
VT Auto Wrap=On
|
||||
Profile File Version=200
|
||||
Kerberos Use Hummingbird Kerberos=On
|
||||
Cryptographic Mode=1
|
||||
Save Mode=0
|
||||
[HEBar-59440]
|
||||
sizeFloatCX=534
|
||||
sizeFloatCY=28
|
||||
[Toolbars Layout-Summary]
|
||||
Bars=3
|
||||
ScreenCX=1366
|
||||
ScreenCY=768
|
||||
[Session Options]
|
||||
Last Selected Page=4000
|
||||
[Toolbars]
|
||||
ToolBarVersion=5
|
||||
bar0=Default VT ToolBar.tbv
|
||||
[Toolbars Layout-Bar0]
|
||||
BarID=59393
|
||||
[Toolbars Layout-Bar1]
|
||||
BarID=59419
|
||||
Bars=3
|
||||
Bar#0=0
|
||||
Bar#1=59440
|
||||
Bar#2=0
|
||||
[Toolbars Layout-Bar2]
|
||||
BarID=59440
|
||||
XPos=-2
|
||||
YPos=-2
|
||||
Docking=1
|
||||
MRUDockID=59419
|
||||
MRUDockLeftPos=-2
|
||||
MRUDockTopPos=-2
|
||||
MRUDockRightPos=541
|
||||
MRUDockBottomPos=26
|
||||
MRUFloatStyle=8196
|
||||
MRUFloatXPos=-1
|
||||
MRUFloatYPos=0
|
||||
[Hotspots]
|
||||
ShowTips=1
|
||||
[ShortcutReplacers]
|
||||
Size=0
|
||||
[Site Info]
|
||||
EnableCache=Yes
|
||||
RefreshCacheOnConnect=0
|
||||
AlwaysUploadUsing=0
|
||||
AlwaysDownloadUsing=0
|
||||
ProfileVersion=5
|
||||
SecurityType=0
|
||||
Port=21
|
||||
SSL Implicit Port=990
|
||||
Directory2=000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
|
||||
SFTPProxyAddress=localhost
|
||||
SFTPProxyPort=0
|
||||
SFTPWriteSize=256
|
||||
GssApiServiceName=host
|
||||
GssApiProtLevel=P
|
||||
GssApiIssueCCC=No
|
||||
GssApiPBSZ=16000
|
||||
GssApiKerbClient=0
|
||||
GssApiUseHMS2MIT=No
|
||||
PassiveMode=2
|
||||
AllowServer2ServerTransfer=0
|
||||
KeepAlive=No
|
||||
AutoDetectServerType=Yes
|
||||
KeepAliveTime=0
|
||||
FirewallPort=21
|
||||
FirewallType=0
|
||||
NetworkTimeout=120
|
||||
ConvertGMTTime=0
|
||||
ResolveLinks=1
|
||||
RootToInitialDir=0
|
||||
ChangeDirChangesRoot=0
|
||||
DropVmsVersion=0
|
||||
LocalHourOffset=0
|
||||
LocalMinuteOffset=0
|
||||
TemporaryProfile=0
|
||||
SSLVersion=1
|
||||
SSLReuse=0
|
||||
SSLUserCertificateMode=0
|
||||
SSLCloseOnNegotiateionFail=0
|
||||
SSLAcceptServerSelfSignedCertificates=Yes
|
||||
SSLAcceptServerUnverifiedCertificates=Yes
|
||||
SSLDataChannelProtected=Yes
|
||||
SSLCryptographicMode=1
|
||||
AllowResume=No
|
||||
EPSV4=0
|
||||
[Stats]
|
||||
NumVisits=0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,122 +0,0 @@
|
||||
# Get-PCProfile.ps1 - Shared helper for resolving the active PC profile
|
||||
# from site-config.json. Dot-source this from any script that needs
|
||||
# profile-aware configuration:
|
||||
#
|
||||
# . "$PSScriptRoot\lib\Get-PCProfile.ps1" (from Shopfloor\ scripts)
|
||||
# . "$PSScriptRoot\Get-PCProfile.ps1" (from lib\ scripts)
|
||||
#
|
||||
# After dot-sourcing, these variables are available:
|
||||
# $siteConfig - full parsed site-config.json (or $null)
|
||||
# $pcType - from pc-type.txt (e.g. "Standard", "CMM", "Display")
|
||||
# $pcSubtype - from pc-subtype.txt (e.g. "Timeclock", "Machine", "Lobby")
|
||||
# $profileKey - lookup key (e.g. "Standard-Machine", "CMM", "Display-Lobby")
|
||||
# $pcProfile - the matched profile object from pcProfiles, or $null
|
||||
#
|
||||
# Resolution order for any config key (e.g. startupItems):
|
||||
# 1. $pcProfile.startupItems (profile-specific override)
|
||||
# 2. $siteConfig.startupItems (site-wide default)
|
||||
# 3. (hardcoded fallback in the calling script)
|
||||
#
|
||||
# Usage pattern in consuming scripts:
|
||||
# $items = Get-ProfileValue 'startupItems'
|
||||
# if ($null -ne $items) { ... } else { <hardcoded fallback> }
|
||||
#
|
||||
# Get-ProfileValue distinguishes "not set" ($null) from "explicitly empty"
|
||||
# (@()) so a profile can opt out of a site-wide default by setting the
|
||||
# key to []. Do NOT use truthiness checks like `if ($pcProfile.startupItems)`
|
||||
# because an empty array is falsy and would fall through to site defaults.
|
||||
|
||||
function Get-ProfileValue {
|
||||
param([Parameter(Mandatory)][string]$Key)
|
||||
if ($pcProfile -and ($pcProfile.PSObject.Properties.Name -contains $Key)) {
|
||||
return ,@($pcProfile.$Key)
|
||||
}
|
||||
if ($siteConfig -and ($siteConfig.PSObject.Properties.Name -contains $Key)) {
|
||||
return ,@($siteConfig.$Key)
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-SiteConfig {
|
||||
$configPath = 'C:\Enrollment\site-config.json'
|
||||
if (-not (Test-Path -LiteralPath $configPath)) {
|
||||
Write-Host "site-config.json not found - using defaults" -ForegroundColor DarkGray
|
||||
return $null
|
||||
}
|
||||
try {
|
||||
return (Get-Content -LiteralPath $configPath -Raw -ErrorAction Stop | ConvertFrom-Json)
|
||||
} catch {
|
||||
Write-Warning "Failed to parse site-config.json: $_"
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
$siteConfig = Get-SiteConfig
|
||||
|
||||
# Read PC type + sub-type from the files startnet.cmd wrote during imaging
|
||||
$pcType = ''
|
||||
$pcSubtype = ''
|
||||
$typeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$subtypeFile = 'C:\Enrollment\pc-subtype.txt'
|
||||
|
||||
if (Test-Path -LiteralPath $typeFile) {
|
||||
$pcType = (Get-Content -LiteralPath $typeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
if (Test-Path -LiteralPath $subtypeFile) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $subtypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Display sub-type fallback: if pc-subtype.txt is absent (post-rename-reorg
|
||||
# default) but display-type.txt exists, use it as the subtype. Lets the
|
||||
# Display-Lobby / Display-Dashboard / gea-shopfloor-display-{lobby,dashboard}
|
||||
# profile keys resolve correctly for Display PCs.
|
||||
$displayTypeFile = 'C:\Enrollment\display-type.txt'
|
||||
if (-not $pcSubtype -and ($pcType -ieq 'gea-shopfloor-display' -or $pcType -ieq 'Display') -and (Test-Path -LiteralPath $displayTypeFile)) {
|
||||
$pcSubtype = (Get-Content -LiteralPath $displayTypeFile -First 1 -ErrorAction SilentlyContinue).Trim()
|
||||
}
|
||||
|
||||
# Build the profile key: "Standard-Machine", "CMM", "Display-Lobby", etc.
|
||||
$profileKey = if ($pcSubtype) { "$pcType-$pcSubtype" } else { $pcType }
|
||||
|
||||
# 2026-05-03 rename: when the legacy key (e.g. "Standard-Machine") is not
|
||||
# present in pcProfiles, fall through to the new gea-shopfloor-* equivalent
|
||||
# (e.g. "gea-shopfloor-collections"). Vice versa: a fleet PC that already
|
||||
# writes "gea-shopfloor-collections" finds its profile under either key.
|
||||
$pcProfileAliasGroups = @(
|
||||
@('Standard-Machine', 'gea-shopfloor-collections', 'gea-shopfloor-nocollections'),
|
||||
@('Standard-Timeclock', 'gea-shopfloor-common'),
|
||||
@('Lab', 'gea-shopfloor-common'),
|
||||
@('CMM', 'gea-shopfloor-cmm'),
|
||||
@('Keyence', 'gea-shopfloor-keyence'),
|
||||
@('WaxAndTrace', 'gea-shopfloor-waxtrace'),
|
||||
@('Genspect', 'gea-shopfloor-genspect'),
|
||||
@('Display', 'gea-shopfloor-display'),
|
||||
@('Display-Lobby', 'gea-shopfloor-display-Lobby', 'gea-shopfloor-display-lobby'),
|
||||
@('Display-Dashboard', 'gea-shopfloor-display-Dashboard', 'gea-shopfloor-display-dashboard'),
|
||||
@('Heattreat', 'gea-shopfloor-heattreat')
|
||||
)
|
||||
|
||||
# Look up the profile in pcProfiles. Fall back to $null (callers use
|
||||
# site-wide defaults or hardcoded values).
|
||||
$pcProfile = $null
|
||||
if ($siteConfig -and $siteConfig.pcProfiles -and $profileKey) {
|
||||
$pcProfile = $siteConfig.pcProfiles.$profileKey
|
||||
if (-not $pcProfile) {
|
||||
foreach ($g in $pcProfileAliasGroups) {
|
||||
if ($g -icontains $profileKey) {
|
||||
foreach ($alias in $g) {
|
||||
if ($alias -ieq $profileKey) { continue }
|
||||
$candidate = $siteConfig.pcProfiles.$alias
|
||||
if ($candidate) { $pcProfile = $candidate; break }
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($pcProfile) {
|
||||
Write-Host "PC profile: $profileKey" -ForegroundColor Cyan
|
||||
} elseif ($profileKey) {
|
||||
Write-Host "PC profile '$profileKey' not found in site-config.json - using site defaults" -ForegroundColor DarkGray
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
# Invoke-FilteredReportIP.ps1
|
||||
# Reports this bay's GE corp IP to the GE Tines webhook with a tight
|
||||
# subnet filter so PXE LAN (10.9.100.x) or any other 10.x stray address
|
||||
# never leaks. Mirrors the payload shape of GE's Intune-deployed
|
||||
# ReportIPAddresses_v2.ps1 so the receiving Tines workflow accepts it.
|
||||
#
|
||||
# WHY:
|
||||
# GE's signed Report IP script filters Get-NetIPAddress with
|
||||
# $_.StartsWith("10.") - too broad for WJ where PXE LAN is also 10.x.
|
||||
# Calling our own POST with a narrow filter guarantees the webhook
|
||||
# only ever sees the bay's AESFMA corp 10.x address.
|
||||
#
|
||||
# ALLOWED RANGES (WJ-specific - update if site moves to a new VLAN):
|
||||
# 10.134.48.0/23 (10.134.48.0 - 10.134.49.255)
|
||||
# 10.48.249.0/26 (10.48.249.0 - 10.48.249.63)
|
||||
|
||||
param(
|
||||
[switch]$Force,
|
||||
[int]$MaxAttempts = 6,
|
||||
[int]$RetrySeconds = 10
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
$logFile = 'C:\Logs\FilteredReportIP.log'
|
||||
New-Item -ItemType Directory -Path 'C:\Logs' -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
|
||||
function Log([string]$msg) {
|
||||
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
|
||||
"$ts $msg" | Tee-Object -FilePath $logFile -Append
|
||||
}
|
||||
|
||||
# Allowed ranges (CIDR -> first IP int + prefix bits)
|
||||
$allowedRanges = @(
|
||||
@{ Network = '10.134.48.0'; PrefixLen = 23 },
|
||||
@{ Network = '10.48.249.0'; PrefixLen = 26 }
|
||||
)
|
||||
|
||||
function ConvertTo-Uint32($ip) {
|
||||
$bytes = ([System.Net.IPAddress]::Parse($ip)).GetAddressBytes()
|
||||
[Array]::Reverse($bytes)
|
||||
return [BitConverter]::ToUInt32($bytes, 0)
|
||||
}
|
||||
|
||||
function Test-InAllowedRange([string]$ip) {
|
||||
try {
|
||||
$ipInt = ConvertTo-Uint32 $ip
|
||||
foreach ($r in $allowedRanges) {
|
||||
$netInt = ConvertTo-Uint32 $r.Network
|
||||
$mask = [uint32]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $r.PrefixLen))
|
||||
if (($ipInt -band $mask) -eq ($netInt -band $mask)) { return $true }
|
||||
}
|
||||
} catch {}
|
||||
return $false
|
||||
}
|
||||
|
||||
Log "=== Filtered Report IP fire ==="
|
||||
$allIPs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.IPAddress -notmatch '^169\.254' -and $_.IPAddress -ne '127.0.0.1' } |
|
||||
Select-Object -ExpandProperty IPAddress
|
||||
Log "All IPv4 addresses on this bay: $($allIPs -join ', ')"
|
||||
|
||||
$match = @($allIPs | Where-Object { Test-InAllowedRange $_ })
|
||||
if ($match.Count -eq 0) {
|
||||
Log "No IP in allowed ranges (10.134.48.0/23 or 10.48.249.0/26). Not POSTing."
|
||||
exit 2
|
||||
}
|
||||
$reportIP = $match[0]
|
||||
Log "Reporting IP: $reportIP"
|
||||
|
||||
$statusFile = "$env:ProgramData\GEA\FilteredReportIP\last-ip.txt"
|
||||
$lastIP = ''
|
||||
if (Test-Path $statusFile) { $lastIP = (Get-Content $statusFile -ErrorAction SilentlyContinue) -join '' }
|
||||
if (-not $Force -and $lastIP -eq $reportIP) {
|
||||
Log "IP unchanged from last POST ($lastIP). Skipping."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$webhookUrl = 'https://tines.apps.geaerospace.com/webhook/aa891efac08bab3d077d9956ab6614d0/c687048cb0ee35c33760ad14cb9257eb'
|
||||
$body = @{
|
||||
host = $env:COMPUTERNAME
|
||||
fqdn = "$env:COMPUTERNAME.device.geaerospace.net"
|
||||
IP = $reportIP
|
||||
force_update = if ($Force) { 'True' } else { 'False' }
|
||||
}
|
||||
|
||||
for ($i = 1; $i -le $MaxAttempts; $i++) {
|
||||
try {
|
||||
Log "POST attempt $i/$MaxAttempts ..."
|
||||
$resp = Invoke-WebRequest -Uri $webhookUrl -Method POST -Body $body -TimeoutSec 60 -UseBasicParsing -ErrorAction Stop
|
||||
Log "POST succeeded. StatusCode=$($resp.StatusCode) Content=$($resp.Content)"
|
||||
New-Item -ItemType Directory -Path (Split-Path $statusFile) -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
Set-Content -Path $statusFile -Value $reportIP -Force
|
||||
exit 0
|
||||
} catch {
|
||||
Log "POST attempt $i failed: $_"
|
||||
if ($i -lt $MaxAttempts) { Start-Sleep -Seconds $RetrySeconds }
|
||||
}
|
||||
}
|
||||
Log "All $MaxAttempts attempts failed - giving up."
|
||||
exit 1
|
||||
@@ -1,68 +0,0 @@
|
||||
# Map-SfldShare.ps1 - Map S: drive on user logon using SFLD creds from HKLM.
|
||||
#
|
||||
# Runs as the interactive user (BUILTIN\Users, Limited) so the drive
|
||||
# mapping lands in the logged-in user's session. Reads username/password
|
||||
# directly from HKLM:\SOFTWARE\GE\SFLD\Credentials\* and passes them
|
||||
# inline to net use -- no Windows Credential Manager involvement.
|
||||
#
|
||||
# Why not the vendor's ConsumeCredentials.ps1: it calls
|
||||
# New-StoredCredential -Persist LocalMachine which requires admin.
|
||||
# ShopFloor is a non-admin user, so the cred-store step fails silently
|
||||
# and the subsequent net use (which relies on those stored creds) has
|
||||
# no authentication. Direct net use /user: bypasses the issue entirely.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$logDir = 'C:\Logs\SFLD'
|
||||
if (-not (Test-Path $logDir)) {
|
||||
try { New-Item -Path $logDir -ItemType Directory -Force | Out-Null } catch { $logDir = $env:TEMP }
|
||||
}
|
||||
$logFile = Join-Path $logDir 'map-share.log'
|
||||
|
||||
function Write-MapLog {
|
||||
param([string]$Message)
|
||||
$line = '[{0}] [{1}] {2}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $env:USERNAME, $Message
|
||||
Add-Content -Path $logFile -Value $line -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-MapLog '=== Map-SfldShare start ==='
|
||||
|
||||
$credsBase = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||
if (-not (Test-Path $credsBase)) {
|
||||
Write-MapLog 'No HKLM SFLD credentials yet - exiting'
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($entry in (Get-ChildItem -Path $credsBase -ErrorAction SilentlyContinue)) {
|
||||
$p = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
|
||||
if (-not $p -or -not $p.TargetHost -or -not $p.Username -or -not $p.Password) { continue }
|
||||
|
||||
$drive = $null
|
||||
$share = $null
|
||||
try { $drive = $p.DriveLetter } catch {}
|
||||
try { $share = $p.ShareName } catch {}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($drive) -or [string]::IsNullOrWhiteSpace($share)) { continue }
|
||||
|
||||
$drive = $drive.TrimEnd(':') + ':'
|
||||
$share = $share.TrimStart('\')
|
||||
$uncPath = "\\$($p.TargetHost)\$share"
|
||||
|
||||
# Skip if already mapped to the right target
|
||||
$existing = & net use $drive 2>&1
|
||||
if ($LASTEXITCODE -eq 0 -and ($existing -join "`n") -match [regex]::Escape($uncPath)) {
|
||||
Write-MapLog "$drive already mapped to $uncPath - skipping"
|
||||
continue
|
||||
}
|
||||
|
||||
& net use $drive /delete /y 2>$null | Out-Null
|
||||
$out = & net use $drive $uncPath /user:$($p.Username) $($p.Password) /persistent:yes 2>&1
|
||||
if ($LASTEXITCODE -eq 0) {
|
||||
Write-MapLog "Mapped $drive -> $uncPath"
|
||||
} else {
|
||||
Write-MapLog "FAILED $drive -> $uncPath (exit $LASTEXITCODE): $out"
|
||||
}
|
||||
}
|
||||
|
||||
Write-MapLog '=== Map-SfldShare end ==='
|
||||
exit 0
|
||||
@@ -249,6 +249,27 @@ function Get-Phase1 {
|
||||
# the force-lockdown marker has been written by sfld_autologon.ps1.
|
||||
# When either signal is present, push idx=8 / status=succeeded so the
|
||||
# dashboard card flips to 100% green.
|
||||
|
||||
# Lockdown handover also ends the imaging autologon budget.
|
||||
# Run-ShopfloorSetup tops AutoLogonCount up to 10 for SupportUser, and the
|
||||
# Sysinternals Autologon run that switches the bay to ShopFloor leaves it
|
||||
# in place. ShopFloor then inherited the remainder and Windows turned
|
||||
# autologon off when it reached 0 - the bay parked at a ShopFloor password
|
||||
# prompt a few reboots after imaging. Delete it as soon as Winlogon shows
|
||||
# ShopFloor. This task runs as Users (Limited) and the HKLM write can be
|
||||
# denied; the SYSTEM "GE Shopfloor Post-PPKG" task (Invoke-PostPpkg.ps1)
|
||||
# does the same cleanup at every startup until it has succeeded.
|
||||
try {
|
||||
$wlKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon'
|
||||
$wlNow = Get-ItemProperty $wlKey -ErrorAction Stop
|
||||
if ($wlNow.DefaultUserName -like 'ShopFloor*' -and $null -ne $wlNow.AutoLogonCount) {
|
||||
Remove-ItemProperty $wlKey -Name AutoLogonCount -ErrorAction Stop
|
||||
Write-Host "Removed leftover AutoLogonCount ($($wlNow.AutoLogonCount)) after ShopFloor handover"
|
||||
}
|
||||
} catch {
|
||||
Write-Warning "Could not clear AutoLogonCount: $_"
|
||||
}
|
||||
|
||||
if (-not $script:cache.LockdownCompletePushed) {
|
||||
$lockdownApplied = $false
|
||||
try {
|
||||
@@ -926,12 +947,9 @@ function Format-Snapshot {
|
||||
@{ Ok = $Snap.Phase3.InstallComplete; Failed = $false }
|
||||
)
|
||||
# Phase 4 (per-script Application Install) was retired 2026-04-28:
|
||||
# apps now deploy via the GE-Enforce manifest engine off
|
||||
# \\tsgwp00525\...\machineapps\, gated on AESFMA WiFi + S: mount
|
||||
# which both happen post-reboot. The monitor's responsibility ends
|
||||
# at "DSC bootstrap + creds + lockdown landed, time to reboot."
|
||||
# The manifest engine takes over from there on the next ShopFloor
|
||||
# logon.
|
||||
# site apps install from the post-PPKG list after the reboot
|
||||
# (Invoke-PostPpkg.ps1). The monitor's responsibility ends at
|
||||
# "DSC bootstrap + creds + lockdown landed, time to reboot."
|
||||
|
||||
$p4Done = ($Snap.Phase5.ConsumeCredsTask -and $Snap.Phase5.CredsPopulated)
|
||||
$p4Status = Get-PhaseStatus @(
|
||||
@@ -1229,7 +1247,7 @@ if ($AsTask -and (Test-Path -LiteralPath $syncCompleteMarker)) {
|
||||
# For these types, enrollment is "done" once Phase 1 (Identity) completes
|
||||
# and policies start arriving - there is no Phase 2-5 to wait for.
|
||||
$pcTypeFile = 'C:\Enrollment\pc-type.txt'
|
||||
$noDscTypes = @('Display', 'gea-shopfloor-display')
|
||||
$noDscTypes = @()
|
||||
$skipDsc = $false
|
||||
if (Test-Path $pcTypeFile) {
|
||||
$pcType = (Get-Content $pcTypeFile -First 1).Trim()
|
||||
@@ -1287,29 +1305,14 @@ try {
|
||||
$qrRefreshed = [bool]($qrText -notmatch 'not yet Azure AD joined')
|
||||
}
|
||||
|
||||
# Filtered Report IP shim - POSTs this bay's GE corp IP to the
|
||||
# Tines webhook directly with a tight subnet filter. Fires each
|
||||
# tick until it succeeds once (script's own status-file dedup
|
||||
# prevents duplicate POSTs). Bypasses the timing window where
|
||||
# GE's Intune-deployed Report IP misses (it may run before the
|
||||
# SCEP cert + AESFMA join land).
|
||||
if (-not $filteredReportIpSucceeded) {
|
||||
$filteredScript = Join-Path $PSScriptRoot 'Invoke-FilteredReportIP.ps1'
|
||||
if (Test-Path $filteredScript) {
|
||||
$rc = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $filteredScript 2>&1 | Out-Null
|
||||
if ($LASTEXITCODE -eq 0) { $filteredReportIpSucceeded = $true }
|
||||
}
|
||||
}
|
||||
|
||||
Clear-Host
|
||||
Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger
|
||||
Write-Host ""
|
||||
Write-Host $qrText
|
||||
|
||||
# Final state: every phase landed. Apps are no longer in the gate -
|
||||
# they deploy via the GE-Enforce manifest engine post-reboot, gated
|
||||
# on AESFMA WiFi + S: drive mount which only happen after we hand
|
||||
# off to the ShopFloor autologon session. So this gate is now:
|
||||
# the site's post-PPKG list installs them after the reboot. So this
|
||||
# gate is now:
|
||||
# - CredsPopulated: SFLD share creds in HKLM (Machine-Enforce,
|
||||
# Acrobat-Enforce, CMM-Enforce, manifest
|
||||
# engine all need these)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
# Restore-EDncReg.ps1 - Import a per-machine eDNC .reg backup from a folder.
|
||||
#
|
||||
# Two callers:
|
||||
# 1. Standard/03-RestoreEDncConfig.ps1 (imaging-time, source = local PXE copy)
|
||||
# 2. Shopfloor/lib/Update-MachineNumber.ps1 (post-image 9999 -> real, source
|
||||
# = tsgwp00525 SFLD share mounted via existing SFLD credential pattern)
|
||||
#
|
||||
# The .reg files live as <num>-<host>.reg or <num>.reg. Returns the path
|
||||
# that was imported, or $null if nothing was found/imported. Errors are
|
||||
# caught and logged via Write-Host (no throwing); callers continue.
|
||||
#
|
||||
# Dot-source with:
|
||||
# . "$PSScriptRoot\lib\Restore-EDncReg.ps1" (from Shopfloor\)
|
||||
# . "$PSScriptRoot\..\Shopfloor\lib\Restore-EDncReg.ps1" (from Standard\)
|
||||
|
||||
function Mount-SFLDShare {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Mounts a \\server\share path using SFLD creds from
|
||||
HKLM:\SOFTWARE\GE\SFLD\Credentials\*. Returns $true on success,
|
||||
$false if creds are missing or mount fails.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SharePath,
|
||||
[string]$DriveLetter = 'V:'
|
||||
)
|
||||
|
||||
$server = ($SharePath -replace '^\\\\', '') -split '\\' | Select-Object -First 1
|
||||
$basePath = 'HKLM:\SOFTWARE\GE\SFLD\Credentials'
|
||||
if (-not (Test-Path $basePath)) { return $false }
|
||||
|
||||
$cred = $null
|
||||
foreach ($entry in Get-ChildItem -Path $basePath -ErrorAction SilentlyContinue) {
|
||||
$props = Get-ItemProperty -Path $entry.PSPath -ErrorAction SilentlyContinue
|
||||
if (-not $props -or -not $props.TargetHost) { continue }
|
||||
if ($props.TargetHost -eq $server -or
|
||||
$props.TargetHost -like "$server.*" -or
|
||||
$server -like "$($props.TargetHost).*") {
|
||||
$cred = $props; break
|
||||
}
|
||||
}
|
||||
if (-not $cred -or -not $cred.Username -or -not $cred.Password) { return $false }
|
||||
|
||||
& net use $DriveLetter /delete /y 2>$null | Out-Null
|
||||
$null = & net use $DriveLetter $SharePath /user:$($cred.Username) $($cred.Password) /persistent:no 2>&1
|
||||
return ($LASTEXITCODE -eq 0)
|
||||
}
|
||||
|
||||
function Import-EDncRegBackup {
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Looks for <MachineNumber>-*.reg or <MachineNumber>.reg in SourceRoot
|
||||
and runs `reg import` on it. Caller is responsible for mounting any
|
||||
network share first; this function works on whatever local or drive-
|
||||
letter path is handed in.
|
||||
.PARAMETER SourceRoot
|
||||
Directory containing the .reg backups.
|
||||
.PARAMETER MachineNumber
|
||||
The target machine number (digits). Used as a filename prefix.
|
||||
.OUTPUTS
|
||||
Path of the imported file, or $null if none found / import failed.
|
||||
#>
|
||||
param(
|
||||
[Parameter(Mandatory)][string]$SourceRoot,
|
||||
[Parameter(Mandatory)][string]$MachineNumber
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $SourceRoot)) {
|
||||
Write-Host " Restore-EDncReg: source root not found: $SourceRoot"
|
||||
return $null
|
||||
}
|
||||
|
||||
$candidate = Get-ChildItem -Path $SourceRoot -Filter "$MachineNumber.reg" -File -ErrorAction SilentlyContinue |
|
||||
Select-Object -First 1
|
||||
if (-not $candidate) {
|
||||
Write-Host " Restore-EDncReg: no backup for machine $MachineNumber in $SourceRoot"
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Host " Restore-EDncReg: importing $($candidate.FullName)"
|
||||
$out = & reg.exe import "$($candidate.FullName)" 2>&1
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host " Restore-EDncReg: reg import failed (exit $LASTEXITCODE): $out"
|
||||
return $null
|
||||
}
|
||||
Write-Host " Restore-EDncReg: imported OK"
|
||||
return $candidate.FullName
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
# Set-OpenTextAutoStart.ps1 - place WJ Shopfloor.lnk in the All Users
|
||||
# Startup folder so HostExplorer's "WJ Shopfloor" session launches at
|
||||
# every login. Idempotent: re-running is a no-op when the .lnk already
|
||||
# exists at the same path.
|
||||
#
|
||||
# Used by per-pc-type 09-Setup scripts for shopfloor types whose only
|
||||
# business app is OpenText (common, waxtrace, genspect, heattreat).
|
||||
# collections + nocollections do NOT auto-start OpenText - their techs
|
||||
# pick which apps via Configure-PC.ps1.
|
||||
#
|
||||
# Source .lnk is created by the OpenText preinstall (Setup-OpenText.ps1)
|
||||
# on the public desktop. If the .lnk is missing, log a warning and exit
|
||||
# 0 - imaging chain still continues; auto-start can be re-attempted on a
|
||||
# subsequent login by re-running this script.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
$startupDir = 'C:\ProgramData\Microsoft\Windows\Start Menu\Programs\StartUp'
|
||||
$publicDesktop = 'C:\Users\Public\Desktop'
|
||||
|
||||
$candidates = @(
|
||||
Join-Path $publicDesktop 'WJ Shopfloor.lnk'
|
||||
Join-Path (Join-Path $publicDesktop 'Shopfloor Tools') 'WJ Shopfloor.lnk'
|
||||
)
|
||||
$src = $candidates | Where-Object { Test-Path -LiteralPath $_ } | Select-Object -First 1
|
||||
|
||||
if (-not $src) {
|
||||
Write-Warning "WJ Shopfloor.lnk not found on public desktop - OpenText auto-start NOT configured."
|
||||
Write-Warning " Searched: $($candidates -join ' ; ')"
|
||||
Write-Warning " Setup-OpenText.ps1 should create it during preinstall - check OpenText install state."
|
||||
return
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $startupDir)) {
|
||||
New-Item -Path $startupDir -ItemType Directory -Force | Out-Null
|
||||
}
|
||||
|
||||
$dst = Join-Path $startupDir 'WJ Shopfloor.lnk'
|
||||
try {
|
||||
Copy-Item -LiteralPath $src -Destination $dst -Force
|
||||
Write-Host "OpenText auto-start enabled: $dst (source: $src)"
|
||||
} catch {
|
||||
Write-Warning "Failed to copy WJ Shopfloor.lnk to startup: $_"
|
||||
}
|
||||
@@ -31,7 +31,7 @@ $ErrorActionPreference = 'Continue'
|
||||
# 1. -QRCoderDllPath if explicitly passed
|
||||
# 2. Next to this script ($PSScriptRoot\QRCoder.dll)
|
||||
# 3. Current working directory
|
||||
# 4. WJ canonical staging path (kept for cross-site portability)
|
||||
# 4. The standard staging path (C:\Enrollment\shopfloor-setup\Shopfloor)
|
||||
# ----------------------------------------------------------------------------
|
||||
function Resolve-QRCoderDll {
|
||||
param([string]$Override)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user