Add fixnetworkshare, winrm-setup-package, udc remote-execution suites
- NetworkDriveManager.ps1: S: drive repair utility - winrm-setup-package: Invoke-RemoteTask helper + Setup-WinRM.bat + HTML guide - remote-execution/udc: UDC_Update.ps1 and batch wrappers for updating DNC controllers on shop-floor PCs - Invoke-RemoteMaintenance.ps1: substantial rework (~1650 lines) - Schedule-Maintenance and complete-asset minor updates - Bump edncfix gitlink to v1.6.0 (2748bfa) - .gitignore: block inventory.csv/xlsx (CUI) and logs_*.txt (per-host logs) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
.gitignore
vendored
7
.gitignore
vendored
@@ -15,6 +15,13 @@ logs/
|
|||||||
# CSV data files (generated)
|
# CSV data files (generated)
|
||||||
applications.csv
|
applications.csv
|
||||||
|
|
||||||
|
# Inventory data (contains CUI / employee SSO / MAC addresses)
|
||||||
|
inventory.csv
|
||||||
|
inventory.xlsx
|
||||||
|
|
||||||
|
# Per-host log files written by remote-execution scripts
|
||||||
|
logs_*.txt
|
||||||
|
|
||||||
# Text files with hostnames/IPs (sensitive)
|
# Text files with hostnames/IPs (sensitive)
|
||||||
computers.txt
|
computers.txt
|
||||||
shopfloor-pcs.txt
|
shopfloor-pcs.txt
|
||||||
|
|||||||
@@ -614,11 +614,19 @@ function Get-PCType {
|
|||||||
if ($domain -eq "logon.ds.ge.com") {
|
if ($domain -eq "logon.ds.ge.com") {
|
||||||
Write-Host " [OK] Shopfloor domain detected" -ForegroundColor Green
|
Write-Host " [OK] Shopfloor domain detected" -ForegroundColor Green
|
||||||
|
|
||||||
# Check for specific machine type applications
|
# Check for specific machine type applications (include per-user installs)
|
||||||
$installedApps = Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
$regPaths = @(
|
||||||
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue |
|
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||||
Where-Object { $_.DisplayName } |
|
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||||
Select-Object -ExpandProperty DisplayName
|
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||||
|
)
|
||||||
|
$installedApps = foreach ($regPath in $regPaths) {
|
||||||
|
if (Test-Path $regPath) {
|
||||||
|
Get-ItemProperty $regPath -ErrorAction SilentlyContinue |
|
||||||
|
Where-Object { $_.DisplayName } |
|
||||||
|
Select-Object -ExpandProperty DisplayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# ================================================================
|
# ================================================================
|
||||||
# PC Type Detection based on installed software
|
# PC Type Detection based on installed software
|
||||||
@@ -821,6 +829,9 @@ function Collect-SystemInfo {
|
|||||||
$installedApps = @()
|
$installedApps = @()
|
||||||
$installedApps += Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName}
|
$installedApps += Get-ItemProperty HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName}
|
||||||
$installedApps += Get-ItemProperty HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName}
|
$installedApps += Get-ItemProperty HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | Where-Object {$_.DisplayName}
|
||||||
|
if (Test-Path "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*") {
|
||||||
|
$installedApps += Get-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*" -ErrorAction SilentlyContinue | Where-Object {$_.DisplayName}
|
||||||
|
}
|
||||||
|
|
||||||
$filteredApps = $installedApps | Select-Object DisplayName, DisplayVersion | Sort-Object DisplayName -Unique
|
$filteredApps = $installedApps | Select-Object DisplayName, DisplayVersion | Sort-Object DisplayName -Unique
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,7 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
- [Overview](#overview)
|
- [Overview](#overview)
|
||||||
|
- [First-Time Setup](#first-time-setup)
|
||||||
- [API Integration](#api-integration)
|
- [API Integration](#api-integration)
|
||||||
- [Prerequisites](#prerequisites)
|
- [Prerequisites](#prerequisites)
|
||||||
- [Quick Start](#quick-start)
|
- [Quick Start](#quick-start)
|
||||||
@@ -17,6 +18,8 @@ Remote maintenance toolkit for executing maintenance tasks on shopfloor PCs via
|
|||||||
- [Service Management](#how-to-manage-services)
|
- [Service Management](#how-to-manage-services)
|
||||||
- [Time Synchronization](#how-to-fix-time-sync-issues)
|
- [Time Synchronization](#how-to-fix-time-sync-issues)
|
||||||
- [DNC Configuration](#how-to-update-dnc-configurations)
|
- [DNC Configuration](#how-to-update-dnc-configurations)
|
||||||
|
- [File Deployment](#how-to-deploy-files)
|
||||||
|
- [Registry Import](#how-to-import-registry-files)
|
||||||
- [Software Deployment](#how-to-deploy-software)
|
- [Software Deployment](#how-to-deploy-software)
|
||||||
- [Batch Operations](#how-to-run-batch-operations)
|
- [Batch Operations](#how-to-run-batch-operations)
|
||||||
- [Targeting Strategies](#targeting-strategies)
|
- [Targeting Strategies](#targeting-strategies)
|
||||||
@@ -32,13 +35,53 @@ This script provides a comprehensive remote maintenance toolkit for managing sho
|
|||||||
**Location:** `S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1`
|
**Location:** `S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1`
|
||||||
|
|
||||||
**Key Features:**
|
**Key Features:**
|
||||||
- 19 maintenance tasks available
|
- 22 maintenance tasks available
|
||||||
|
- General-purpose file deployment (`CopyFile`) and registry import (`ImportReg`) tasks
|
||||||
|
- Registry imports run as logged-in user via scheduled task (HKCU support)
|
||||||
|
- Optional post-copy commands via scheduled task (service/app restarts)
|
||||||
- Multiple targeting options (by name, type, business unit, or all)
|
- Multiple targeting options (by name, type, business unit, or all)
|
||||||
- Concurrent execution with configurable throttling
|
- Concurrent execution with configurable throttling
|
||||||
- Integration with ShopDB for PC discovery
|
- Integration with ShopDB for PC discovery
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
Before running this script for the first time, you must allow PowerShell script execution and unblock the script file.
|
||||||
|
|
||||||
|
### Step 1: Allow PowerShell Script Execution
|
||||||
|
|
||||||
|
Open PowerShell **as Administrator** and run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy RemoteSigned -Scope CurrentUser
|
||||||
|
```
|
||||||
|
|
||||||
|
When prompted, type `Y` and press Enter. This allows locally-created scripts to run and requires downloaded scripts to be signed or unblocked.
|
||||||
|
|
||||||
|
> **Note:** You only need to do this once per user account. `RemoteSigned` is the recommended policy — it allows local scripts while still protecting against untrusted downloads.
|
||||||
|
|
||||||
|
### Step 2: Unblock the Script File
|
||||||
|
|
||||||
|
Files downloaded from a network share or the internet are marked as "blocked" by Windows. You must unblock the script before it can run.
|
||||||
|
|
||||||
|
**Option A — File Explorer (GUI):**
|
||||||
|
|
||||||
|
1. Navigate to `S:\dt\shopfloor\scripts\remote-execution\`
|
||||||
|
2. Right-click `Invoke-RemoteMaintenance.ps1` and select **Properties**
|
||||||
|
3. At the bottom of the General tab, check the **Unblock** checkbox
|
||||||
|
4. Click **Apply**, then **OK**
|
||||||
|
|
||||||
|
**Option B — PowerShell:**
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Unblock-File -Path "S:\dt\shopfloor\scripts\remote-execution\Invoke-RemoteMaintenance.ps1"
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** If you skip this step, you will get a security error when trying to run the script: *"cannot be loaded because running scripts is disabled on this system"* or *"cannot be loaded. The file is not digitally signed."*
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## API Integration
|
## API Integration
|
||||||
|
|
||||||
When using `-All`, `-PcType`, or `-BusinessUnit` targeting, the script retrieves PC lists from the ShopDB API:
|
When using `-All`, `-PcType`, or `-BusinessUnit` targeting, the script retrieves PC lists from the ShopDB API:
|
||||||
@@ -53,12 +96,12 @@ GET /api.asp?action=getShopfloorPCs&businessunitid=1 # Specific business unit
|
|||||||
|
|
||||||
| ID | Type | ID | Type |
|
| ID | Type | ID | Type |
|
||||||
|----|------|----|------|
|
|----|------|----|------|
|
||||||
| 1 | Shopfloor | 7 | Heat Treat |
|
| 1 | Standard | 7 | Keyence |
|
||||||
| 2 | CMM | 8 | Engineer |
|
| 2 | Engineer | 8 | Genspect |
|
||||||
| 3 | Wax Trace | 9 | Standard |
|
| 3 | Shopfloor | 9 | Heat Treat |
|
||||||
| 4 | Keyence | 10 | Inspection |
|
| 4 | Uncategorized | 10 | Inspection |
|
||||||
| 5 | EAS1000 | 11 | Dashboard |
|
| 5 | CMM | 11 | Dashboard |
|
||||||
| 6 | Genspect | 12 | Lobby Display |
|
| 6 | Wax / Trace | 12 | Lobby Display |
|
||||||
|
|
||||||
**See:** [ShopDB API Reference](ShopDB-API.html) for complete API documentation.
|
**See:** [ShopDB API Reference](ShopDB-API.html) for complete API documentation.
|
||||||
|
|
||||||
@@ -133,6 +176,15 @@ The script outputs status for each PC:
|
|||||||
|-----------|------|-------------|
|
|-----------|------|-------------|
|
||||||
| `-Task` | string | Maintenance task to execute |
|
| `-Task` | string | Maintenance task to execute |
|
||||||
|
|
||||||
|
### File Deployment Parameters (CopyFile / ImportReg)
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
|-----------|------|-------------|
|
||||||
|
| `-SourcePath` | string | Source file path (local or UNC). Required for CopyFile and ImportReg |
|
||||||
|
| `-DestinationPath` | string | Destination file path on remote PCs. Required for CopyFile |
|
||||||
|
| `-RunCommand` | string | Command to run after CopyFile. Runs as logged-in user by default (via scheduled task) |
|
||||||
|
| `-AsSystem` | switch | Run ImportReg and -RunCommand in the WinRM session (SYSTEM context) instead of as logged-in user |
|
||||||
|
|
||||||
### Optional Parameters
|
### Optional Parameters
|
||||||
|
|
||||||
| Parameter | Type | Default | Description |
|
| Parameter | Type | Default | Description |
|
||||||
@@ -140,7 +192,8 @@ The script outputs status for each PC:
|
|||||||
| `-Credential` | PSCredential | Prompt | Remote authentication |
|
| `-Credential` | PSCredential | Prompt | Remote authentication |
|
||||||
| `-ApiUrl` | string | Production | ShopDB API endpoint |
|
| `-ApiUrl` | string | Production | ShopDB API endpoint |
|
||||||
| `-ThrottleLimit` | int | 5 | Max concurrent sessions |
|
| `-ThrottleLimit` | int | 5 | Max concurrent sessions |
|
||||||
| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for resolution |
|
| `-DnsSuffix` | string | logon.ds.ge.com | DNS suffix for FQDN resolution |
|
||||||
|
| `-LogFile` | switch | Off | Enable transcript logging to `logs/` directory |
|
||||||
|
|
||||||
### PC Types
|
### PC Types
|
||||||
|
|
||||||
@@ -194,13 +247,22 @@ TBD, Blisk, HPT, Spools, Inspection, Venture, Turn/Burn, DT
|
|||||||
|
|
||||||
| Task | Description | Duration | Impact |
|
| Task | Description | Duration | Impact |
|
||||||
|------|-------------|----------|--------|
|
|------|-------------|----------|--------|
|
||||||
| `UpdateEMxAuthToken` | Update eMx auth from share | 1-2 min | None |
|
| `UpdateDNCMXHosts` | Update FtpHostPrimary/Secondary in DNC\MX registry | <1 min | None |
|
||||||
| `DeployUDCWebServerConfig` | Deploy UDC config | 1-2 min | None |
|
| `AuditDNCConfig` | Compare DNC registry vs UDC backup JSON, export CSV | 1-5 min | None |
|
||||||
|
| `CheckDefectTracker` | Check if Defect_Tracker.exe is running, export CSV | 1-5 min | None |
|
||||||
|
|
||||||
|
### File Deployment Tasks
|
||||||
|
|
||||||
|
| Task | Description | Duration | Impact |
|
||||||
|
|------|-------------|----------|--------|
|
||||||
|
| `CopyFile` | Copy file from `-SourcePath` to `-DestinationPath` on remote PCs | 1-2 min | Low |
|
||||||
|
| `ImportReg` | Copy `.reg` file and import via scheduled task as logged-in user | 1-2 min | Low |
|
||||||
|
|
||||||
### System Tasks
|
### System Tasks
|
||||||
|
|
||||||
| Task | Description | Duration | Impact |
|
| Task | Description | Duration | Impact |
|
||||||
|------|-------------|----------|--------|
|
|------|-------------|----------|--------|
|
||||||
|
| `GPUpdate` | Force Group Policy refresh (`gpupdate /force`) | <1 min | Low |
|
||||||
| `Reboot` | Restart PC (30s delay) | 2-5 min | High |
|
| `Reboot` | Restart PC (30s delay) | 2-5 min | High |
|
||||||
|
|
||||||
### Software Deployment Tasks
|
### Software Deployment Tasks
|
||||||
@@ -222,18 +284,19 @@ Deployment tasks require source files to be available before execution:
|
|||||||
|
|
||||||
| Task | Source File Path |
|
| Task | Source File Path |
|
||||||
|------|------------------|
|
|------|------------------|
|
||||||
| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` |
|
| `InstallDashboard` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\Dashboard\GEAerospaceDashboardSetup.exe` |
|
||||||
| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` |
|
| `InstallLobbyDisplay` | `\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\LobbyDisplay\GEAerospaceLobbyDisplaySetup.exe` |
|
||||||
| `UpdateEMxAuthToken` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt` |
|
| `CopyFile` | Any file - specified via `-SourcePath` parameter |
|
||||||
| `DeployUDCWebServerConfig` | `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\UDC\udc_webserver_settings.json` |
|
| `ImportReg` | Any `.reg` file - specified via `-SourcePath` parameter |
|
||||||
|
|
||||||
### How Deployment Works
|
### How Deployment Works
|
||||||
|
|
||||||
1. **Pre-flight Check:** Script verifies source file exists
|
1. **Pre-flight Check:** Script verifies source file exists
|
||||||
2. **WinRM Session:** Opens remote session to target PC
|
2. **WinRM Session:** Opens remote session to target PC
|
||||||
3. **File Push:** Copies source file to `C:\Windows\Temp\` on remote PC
|
3. **File Push:** Copies source file to `C:\Windows\Temp\` on remote PC
|
||||||
4. **Execution:** Runs install/copy task using pushed file
|
4. **Execution:** Runs install/copy/import task using pushed file
|
||||||
5. **Cleanup:** Removes temp file from remote PC
|
5. **Post-action:** Optionally runs command as logged-in user via scheduled task
|
||||||
|
6. **Cleanup:** Removes temp file from remote PC
|
||||||
|
|
||||||
```
|
```
|
||||||
+---------------------+ WinRM +---------------------+
|
+---------------------+ WinRM +---------------------+
|
||||||
@@ -242,50 +305,44 @@ Deployment tasks require source files to be available before execution:
|
|||||||
| Source Files: | Push File | Temp Location: |
|
| Source Files: | Push File | Temp Location: |
|
||||||
| - Setup.exe | ------------> | C:\Windows\Temp |
|
| - Setup.exe | ------------> | C:\Windows\Temp |
|
||||||
| - config.json | | |
|
| - config.json | | |
|
||||||
| - eMxInfo.txt | Execute | Final Location: |
|
| - settings.reg | Execute | Final Location: |
|
||||||
| (network) | ------------> | C:\Program Files |
|
| (any path/UNC) | ------------> | -DestinationPath |
|
||||||
|
| | | |
|
||||||
|
| | Sched. Task | Logged-in User: |
|
||||||
|
| | ------------> | - regedit /s |
|
||||||
|
| | | - RunCommand |
|
||||||
+---------------------+ +---------------------+
|
+---------------------+ +---------------------+
|
||||||
```
|
```
|
||||||
|
|
||||||
### Directory Structure
|
### CopyFile Details
|
||||||
|
|
||||||
Ensure your script directory contains the required files:
|
The `CopyFile` task:
|
||||||
|
|
||||||
```
|
1. **Source:** Any file specified via `-SourcePath` (local or UNC path)
|
||||||
S:\dt\shopfloor\scripts\remote-execution\
|
2. **Destination:** Specified via `-DestinationPath`
|
||||||
├── Invoke-RemoteMaintenance.ps1
|
3. **Backup:** Existing file is backed up as `<name>-old-<timestamp>.<ext>`
|
||||||
├── GEAerospaceDashboardSetup.exe # For InstallDashboard
|
4. **Post-action:** If `-RunCommand` is specified, runs as the logged-in user via a one-shot scheduled task (same pattern as Dashboard/Lobby kiosk relaunch)
|
||||||
├── GEAerospaceLobbyDisplaySetup.exe # For InstallLobbyDisplay
|
|
||||||
└── udc_webserver_settings.json # For DeployUDCWebServerConfig
|
|
||||||
```
|
|
||||||
|
|
||||||
### eMx Auth Token Details
|
### ImportReg Details
|
||||||
|
|
||||||
The `UpdateEMxAuthToken` task:
|
The `ImportReg` task:
|
||||||
|
|
||||||
1. **Source:** `\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\eMx\eMxInfo.txt`
|
1. **Source:** `.reg` file specified via `-SourcePath`
|
||||||
2. **Destinations:** (both paths if they exist)
|
2. **Import method:** `regedit.exe /s` via one-shot scheduled task as logged-in user
|
||||||
- `C:\Program Files\GE Aircraft Engines\DNC\eMxInfo.txt`
|
3. **HKCU support:** Runs as the logged-in user, so both HKLM and HKCU keys apply correctly
|
||||||
- `C:\Program Files (x86)\GE Aircraft Engines\DNC\eMxInfo.txt`
|
4. **Fallback:** If no user is logged in, runs `regedit.exe /s` directly (HKLM only)
|
||||||
3. **Backup:** Creates `eMxInfo-old-YYYYMMDD-HHMMSS.txt` before overwriting
|
5. **Cleanup:** Temp `.reg` file removed after import
|
||||||
4. **Post-action:** Restarts DNC service (`LDnc.exe`)
|
|
||||||
|
|
||||||
### UDC Web Server Config Details
|
|
||||||
|
|
||||||
The `DeployUDCWebServerConfig` task:
|
|
||||||
|
|
||||||
1. **Pre-check:** Verifies UDC is installed (`C:\Program Files\UDC` exists)
|
|
||||||
2. **Skip:** PCs without UDC are skipped (not counted as failures)
|
|
||||||
3. **Destination:** `C:\ProgramData\UDC\udc_webserver_settings.json`
|
|
||||||
4. **Backup:** Creates backup before overwriting
|
|
||||||
|
|
||||||
### Dashboard/Lobby Display Install Details
|
### Dashboard/Lobby Display Install Details
|
||||||
|
|
||||||
Both kiosk app installers:
|
Both kiosk app installers:
|
||||||
|
|
||||||
1. **Installer type:** Inno Setup (supports `/VERYSILENT`)
|
1. **Installer type:** Inno Setup (supports `/VERYSILENT`)
|
||||||
2. **Execution:** Silent install with no user prompts
|
2. **Pre-install:** Kills running Edge kiosk via `PrepareToInstall` in Inno Setup
|
||||||
3. **Cleanup:** Installer removed from temp after execution
|
3. **Execution:** Silent install with no user prompts (120-second timeout)
|
||||||
|
4. **Post-install:** Creates a one-shot scheduled task to relaunch Edge in kiosk mode as the logged-in user (e.g. `lg044513sd`), then auto-deletes the task
|
||||||
|
5. **Cleanup:** Installer removed from temp after execution
|
||||||
|
6. **Connectivity:** Offline PCs are skipped with a ping check before connecting
|
||||||
|
|
||||||
**Uninstall GUIDs:**
|
**Uninstall GUIDs:**
|
||||||
- Dashboard: `{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}`
|
- Dashboard: `{9D9EEE25-4D24-422D-98AF-2ADEDA4745ED}`
|
||||||
@@ -295,7 +352,7 @@ Both kiosk app installers:
|
|||||||
|
|
||||||
To add a new application for deployment, edit the script in two places:
|
To add a new application for deployment, edit the script in two places:
|
||||||
|
|
||||||
**Step 1: Add to `$KioskAppConfig` hashtable (~line 1388)**
|
**Step 1: Add to `$KioskAppConfig` hashtable**
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
$KioskAppConfig = @{
|
$KioskAppConfig = @{
|
||||||
@@ -304,10 +361,11 @@ $KioskAppConfig = @{
|
|||||||
# Add new application
|
# Add new application
|
||||||
'InstallNewApp' = @{
|
'InstallNewApp' = @{
|
||||||
Action = 'Install'
|
Action = 'Install'
|
||||||
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
|
InstallerPath = '\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe'
|
||||||
InstallerName = 'NewAppSetup.exe'
|
InstallerName = 'NewAppSetup.exe'
|
||||||
AppName = 'New Application Name'
|
AppName = 'New Application Name'
|
||||||
UninstallGuid = '{YOUR-GUID-HERE}' # Find in registry after manual install
|
UninstallGuid = '{YOUR-GUID-HERE}' # Find in registry after manual install
|
||||||
|
KioskUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb/your-page/' # Optional: relaunch Edge kiosk after install
|
||||||
}
|
}
|
||||||
'UninstallNewApp' = @{
|
'UninstallNewApp' = @{
|
||||||
Action = 'Uninstall'
|
Action = 'Uninstall'
|
||||||
@@ -334,7 +392,7 @@ $KioskAppConfig = @{
|
|||||||
**Step 3: Place installer on network share**
|
**Step 3: Place installer on network share**
|
||||||
|
|
||||||
```
|
```
|
||||||
\\tsgwp00525.wjs.geaerospace.net\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
|
\\tsgwp00525.wjs.geaerospace.net\shared\dt\shopfloor\scripts\NewApp\NewAppSetup.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
**Finding the Uninstall GUID:**
|
**Finding the Uninstall GUID:**
|
||||||
@@ -562,37 +620,129 @@ wuauclt /detectnow
|
|||||||
|
|
||||||
### How to Update DNC Configurations
|
### How to Update DNC Configurations
|
||||||
|
|
||||||
#### Update eMx Authentication Token
|
#### Update DNC MX Hosts
|
||||||
|
|
||||||
**Scenario:** eMx authentication is failing on DNC PCs.
|
**Scenario:** FtpHostPrimary/FtpHostSecondary in DNC\MX registry needs updating (hostname migration).
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Single PC
|
# Single PC
|
||||||
.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateEMxAuthToken -Credential $cred
|
.\Invoke-RemoteMaintenance.ps1 -ComputerName "DNC-PC01" -Task UpdateDNCMXHosts -Credential $cred
|
||||||
|
|
||||||
# All shopfloor PCs
|
# All shopfloor PCs
|
||||||
.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateEMxAuthToken -Credential $cred
|
.\Invoke-RemoteMaintenance.ps1 -All -Task UpdateDNCMXHosts -Credential $cred
|
||||||
```
|
```
|
||||||
|
|
||||||
**What it does:**
|
**What it does:**
|
||||||
1. Backs up existing `eMxInfo.txt` with timestamp
|
1. Checks both 32-bit (WOW6432Node) and 64-bit registry paths
|
||||||
2. Copies new token file from network share
|
2. Only updates values matching the old hostname - skips unexpected values
|
||||||
3. Verifies file was updated
|
3. Safe to run on all PCs: no-ops if DNC\MX key doesn't exist
|
||||||
|
|
||||||
#### Deploy UDC Web Server Configuration
|
#### Audit DNC Config vs UDC Backup
|
||||||
|
|
||||||
**Scenario:** UDC web server settings need to be updated.
|
**Scenario:** Verify DNC registry settings match UDC backup JSON files.
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Deploy to PCs with UDC installed
|
.\Invoke-RemoteMaintenance.ps1 -All -Task AuditDNCConfig -Credential $cred -LogFile
|
||||||
.\Invoke-RemoteMaintenance.ps1 -ComputerName "UDC-PC01","UDC-PC02" -Task DeployUDCWebServerConfig -Credential $cred
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**What it does:**
|
**What it does:**
|
||||||
1. Checks if UDC is installed
|
1. Reads DNC registry values (General, eFocas, Hssb, PPDCS keys)
|
||||||
2. Backs up existing configuration
|
2. Compares against UDC backup JSON files on the network share
|
||||||
3. Deploys new web server settings
|
3. Reports MATCH/MISMATCH/MISSING for each field
|
||||||
4. Does NOT restart UDC (requires manual restart)
|
4. Exports CSV report to `logs/`
|
||||||
|
|
||||||
|
#### Check Defect Tracker Status
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CheckDefectTracker -Credential $cred
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Checks if `Defect_Tracker.exe` is running on each PC
|
||||||
|
2. Reports machine number + running status
|
||||||
|
3. Exports CSV report to `logs/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### How to Deploy Files
|
||||||
|
|
||||||
|
**Scenario:** Push any file to remote PCs with automatic backup of existing files.
|
||||||
|
|
||||||
|
#### Basic File Copy
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Copy a config file to a specific destination
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01","PC02" -Task CopyFile `
|
||||||
|
-SourcePath "\\server\share\config.json" `
|
||||||
|
-DestinationPath "C:\ProgramData\App\config.json" `
|
||||||
|
-Credential $cred
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does:**
|
||||||
|
1. Copies source file to `C:\Windows\Temp\` on remote PC via WinRM
|
||||||
|
2. Creates backup of existing file (if any) with timestamp
|
||||||
|
3. Moves temp file to final destination
|
||||||
|
4. Verifies deployment
|
||||||
|
|
||||||
|
#### File Copy with Post-Copy Command
|
||||||
|
|
||||||
|
**Scenario:** Deploy a file and restart a service/app in the logged-in user's session.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Deploy eMxInfo.txt and kill DNC so it picks up the new file
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -All -Task CopyFile `
|
||||||
|
-SourcePath "\\server\share\eMxInfo.txt" `
|
||||||
|
-DestinationPath "C:\Program Files (x86)\DNC\Server Files\eMxInfo.txt" `
|
||||||
|
-RunCommand "taskkill /IM DNCMain.exe /F" `
|
||||||
|
-Credential $cred
|
||||||
|
|
||||||
|
# Deploy UDC config (no restart needed)
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task CopyFile `
|
||||||
|
-SourcePath "\\server\share\udc_webserver_settings.json" `
|
||||||
|
-DestinationPath "C:\ProgramData\UDC\udc_webserver_settings.json" `
|
||||||
|
-Credential $cred
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-RunCommand` runs via a one-shot scheduled task as the logged-in user, so it works for user-session processes (same pattern as Dashboard/Lobby Display kiosk relaunch).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### How to Import Registry Files
|
||||||
|
|
||||||
|
**Scenario:** Apply registry settings from a `.reg` file to remote PCs.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Import a .reg file on all shopfloor PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task ImportReg `
|
||||||
|
-SourcePath "\\server\share\intranet-zone.reg" `
|
||||||
|
-Credential $cred
|
||||||
|
|
||||||
|
# Import on specific PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -ComputerName "PC01" -Task ImportReg `
|
||||||
|
-SourcePath "C:\Scripts\my-settings.reg" `
|
||||||
|
-Credential $cred
|
||||||
|
```
|
||||||
|
|
||||||
|
**What it does (default — as logged-in user):**
|
||||||
|
1. Copies `.reg` file to `C:\Windows\Temp\` on remote PC via WinRM
|
||||||
|
2. Creates a one-shot scheduled task as the logged-in user
|
||||||
|
3. Runs `regedit.exe /s` to silently import the registry file
|
||||||
|
4. Cleans up temp file and scheduled task
|
||||||
|
|
||||||
|
**HKCU support:** Because the import runs as the logged-in user (via scheduled task), both `HKLM` and `HKCU` keys in the `.reg` file are applied correctly. If no user is logged in, it falls back to direct import (HKLM only).
|
||||||
|
|
||||||
|
#### HKLM-Only / System Context
|
||||||
|
|
||||||
|
Use `-AsSystem` when the `.reg` file only contains `HKLM` keys and you want to skip the scheduled task overhead:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Import HKLM-only registry settings directly as SYSTEM
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -All -Task ImportReg `
|
||||||
|
-SourcePath "\\server\share\machine-policy.reg" `
|
||||||
|
-AsSystem -Credential $cred
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs `regedit.exe /s` directly in the WinRM session (SYSTEM context). Faster, but HKCU keys will not apply to any user.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -612,22 +762,21 @@ $kiosks = @("KIOSK-01", "KIOSK-02", "KIOSK-03")
|
|||||||
```
|
```
|
||||||
|
|
||||||
**What it does:**
|
**What it does:**
|
||||||
1. Copies installer from network share
|
1. Pings target PC (skips if offline)
|
||||||
2. Runs silent installation
|
2. Copies installer from network share to `C:\Windows\Temp\`
|
||||||
3. Configures auto-start
|
3. Kills running Edge kiosk
|
||||||
4. Cleans up installer
|
4. Runs silent installation (120-second timeout)
|
||||||
|
5. Relaunches Edge kiosk via scheduled task as the logged-in user
|
||||||
|
6. Cleans up installer and scheduled task
|
||||||
|
|
||||||
**After installation:**
|
**No reboot required** — Edge relaunches automatically in the logged-in user's session.
|
||||||
- Run data collection to update PC type
|
|
||||||
- Reboot PC to complete setup
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
# Complete Dashboard deployment sequence
|
# Deploy to all Dashboard kiosks
|
||||||
.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task InstallDashboard -Credential $cred
|
.\Invoke-RemoteMaintenance.ps1 -PcType Dashboard -Task InstallDashboard -Credential $cred
|
||||||
.\Invoke-RemoteMaintenance.ps1 -ComputerName "KIOSK-01" -Task Reboot -Credential $cred
|
|
||||||
|
|
||||||
# After reboot, update ShopDB
|
# Deploy to all Lobby Display kiosks
|
||||||
.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "KIOSK-01" -Credential $cred
|
.\Invoke-RemoteMaintenance.ps1 -PcType "Lobby Display" -Task InstallLobbyDisplay -Credential $cred
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Install Lobby Display
|
#### Install Lobby Display
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ powershell-scripts/
|
|||||||
## Table of Contents
|
## Table of Contents
|
||||||
|
|
||||||
1. [Asset Collection Scripts](#asset-collection-scripts) (`asset-collection/`)
|
1. [Asset Collection Scripts](#asset-collection-scripts) (`asset-collection/`)
|
||||||
2. [Remote Execution Scripts](#remote-execution-scripts) (`remote-execution/`)
|
2. [Remote Execution Scripts](#remote-execution-scripts) (`S:\dt\shopfloor\scripts\remote-execution\`)
|
||||||
3. [Setup & Utility Scripts](#setup--utility-scripts) (`setup-utilities/`)
|
3. [Setup & Utility Scripts](#setup--utility-scripts) (`setup-utilities/`)
|
||||||
4. [Registry Backup Scripts](#registry-backup-scripts) (`registry-backup/`)
|
4. [Registry Backup Scripts](#registry-backup-scripts) (`registry-backup/`)
|
||||||
5. [WinRM HTTPS Scripts](#winrm-https-scripts) (`winrm-https/`)
|
5. [WinRM HTTPS Scripts](#winrm-https-scripts) (`winrm-https/`)
|
||||||
|
|||||||
308
docs/convert_to_docx.py
Normal file
308
docs/convert_to_docx.py
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Convert Markdown documentation to Word documents (.docx)
|
||||||
|
With proper code block formatting (shaded boxes)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from docx import Document
|
||||||
|
from docx.shared import Inches, Pt, RGBColor, Twips
|
||||||
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
|
from docx.enum.style import WD_STYLE_TYPE
|
||||||
|
from docx.enum.table import WD_TABLE_ALIGNMENT
|
||||||
|
from docx.oxml.ns import qn, nsmap
|
||||||
|
from docx.oxml import OxmlElement
|
||||||
|
|
||||||
|
def set_cell_shading(cell, color="E8E8E8"):
|
||||||
|
"""Set cell background shading color."""
|
||||||
|
tc = cell._tc
|
||||||
|
tcPr = tc.get_or_add_tcPr()
|
||||||
|
shd = OxmlElement('w:shd')
|
||||||
|
shd.set(qn('w:fill'), color)
|
||||||
|
shd.set(qn('w:val'), 'clear')
|
||||||
|
tcPr.append(shd)
|
||||||
|
|
||||||
|
def set_cell_borders(cell, color="CCCCCC"):
|
||||||
|
"""Set cell border color."""
|
||||||
|
tc = cell._tc
|
||||||
|
tcPr = tc.get_or_add_tcPr()
|
||||||
|
tcBorders = OxmlElement('w:tcBorders')
|
||||||
|
for border_name in ['top', 'left', 'bottom', 'right']:
|
||||||
|
border = OxmlElement(f'w:{border_name}')
|
||||||
|
border.set(qn('w:val'), 'single')
|
||||||
|
border.set(qn('w:sz'), '4')
|
||||||
|
border.set(qn('w:color'), color)
|
||||||
|
tcBorders.append(border)
|
||||||
|
tcPr.append(tcBorders)
|
||||||
|
|
||||||
|
def add_code_block(doc, code_text, language=""):
|
||||||
|
"""Add a formatted code block with shading."""
|
||||||
|
# Create a single-cell table for the code block
|
||||||
|
table = doc.add_table(rows=1, cols=1)
|
||||||
|
table.autofit = True
|
||||||
|
|
||||||
|
cell = table.rows[0].cells[0]
|
||||||
|
|
||||||
|
# Set cell shading (light gray background)
|
||||||
|
set_cell_shading(cell, "F5F5F5")
|
||||||
|
set_cell_borders(cell, "DDDDDD")
|
||||||
|
|
||||||
|
# Clear default paragraph and add code
|
||||||
|
cell.paragraphs[0].clear()
|
||||||
|
|
||||||
|
# Add each line of code
|
||||||
|
lines = code_text.split('\n')
|
||||||
|
for i, line in enumerate(lines):
|
||||||
|
if i == 0:
|
||||||
|
para = cell.paragraphs[0]
|
||||||
|
else:
|
||||||
|
para = cell.add_paragraph()
|
||||||
|
|
||||||
|
para.paragraph_format.space_before = Pt(0)
|
||||||
|
para.paragraph_format.space_after = Pt(0)
|
||||||
|
para.paragraph_format.line_spacing = 1.0
|
||||||
|
|
||||||
|
run = para.add_run(line if line else ' ') # Use space for empty lines
|
||||||
|
run.font.name = 'Consolas'
|
||||||
|
run.font.size = Pt(9)
|
||||||
|
run.font.color.rgb = RGBColor(0, 0, 0)
|
||||||
|
|
||||||
|
# Add spacing after the code block
|
||||||
|
doc.add_paragraph()
|
||||||
|
|
||||||
|
def parse_markdown(md_content):
|
||||||
|
"""Parse markdown content into structured elements."""
|
||||||
|
lines = md_content.split('\n')
|
||||||
|
elements = []
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
# Skip empty lines
|
||||||
|
if not line.strip():
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Headers
|
||||||
|
if line.startswith('# '):
|
||||||
|
elements.append(('h1', line[2:].strip()))
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('## '):
|
||||||
|
elements.append(('h2', line[3:].strip()))
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('### '):
|
||||||
|
elements.append(('h3', line[4:].strip()))
|
||||||
|
i += 1
|
||||||
|
elif line.startswith('#### '):
|
||||||
|
elements.append(('h4', line[5:].strip()))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Horizontal rule
|
||||||
|
elif line.strip() == '---':
|
||||||
|
elements.append(('hr', ''))
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
# Code blocks
|
||||||
|
elif line.strip().startswith('```'):
|
||||||
|
code_lang = line.strip()[3:]
|
||||||
|
code_lines = []
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and not lines[i].strip().startswith('```'):
|
||||||
|
code_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
# Store language info with code
|
||||||
|
elements.append(('code', (code_lang, '\n'.join(code_lines))))
|
||||||
|
i += 1 # Skip closing ```
|
||||||
|
|
||||||
|
# Tables
|
||||||
|
elif '|' in line and i + 1 < len(lines) and '---' in lines[i + 1]:
|
||||||
|
table_lines = [line]
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and '|' in lines[i]:
|
||||||
|
table_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
elements.append(('table', table_lines))
|
||||||
|
|
||||||
|
# Bullet lists
|
||||||
|
elif line.strip().startswith('- ') or line.strip().startswith('* '):
|
||||||
|
list_items = []
|
||||||
|
while i < len(lines) and (lines[i].strip().startswith('- ') or lines[i].strip().startswith('* ') or (lines[i].startswith(' ') and lines[i].strip())):
|
||||||
|
if lines[i].strip().startswith('- ') or lines[i].strip().startswith('* '):
|
||||||
|
list_items.append(lines[i].strip()[2:])
|
||||||
|
elif lines[i].startswith(' ') and list_items:
|
||||||
|
list_items[-1] += ' ' + lines[i].strip()
|
||||||
|
i += 1
|
||||||
|
elements.append(('bullet', list_items))
|
||||||
|
|
||||||
|
# Numbered lists
|
||||||
|
elif re.match(r'^\d+\.\s', line.strip()):
|
||||||
|
list_items = []
|
||||||
|
while i < len(lines) and (re.match(r'^\d+\.\s', lines[i].strip()) or lines[i].startswith(' ')):
|
||||||
|
if re.match(r'^\d+\.\s', lines[i].strip()):
|
||||||
|
list_items.append(re.sub(r'^\d+\.\s', '', lines[i].strip()))
|
||||||
|
elif lines[i].startswith(' ') and list_items:
|
||||||
|
list_items[-1] += ' ' + lines[i].strip()
|
||||||
|
i += 1
|
||||||
|
elements.append(('numbered', list_items))
|
||||||
|
|
||||||
|
# Regular paragraph
|
||||||
|
else:
|
||||||
|
para_lines = [line]
|
||||||
|
i += 1
|
||||||
|
while i < len(lines) and lines[i].strip() and not lines[i].startswith('#') and not lines[i].startswith('```') and not lines[i].startswith('- ') and not lines[i].startswith('* ') and '|' not in lines[i] and not re.match(r'^\d+\.\s', lines[i].strip()):
|
||||||
|
para_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
elements.append(('para', ' '.join(para_lines)))
|
||||||
|
|
||||||
|
return elements
|
||||||
|
|
||||||
|
def clean_text(text):
|
||||||
|
"""Remove markdown formatting from text."""
|
||||||
|
# Bold
|
||||||
|
text = re.sub(r'\*\*([^*]+)\*\*', r'\1', text)
|
||||||
|
# Italic
|
||||||
|
text = re.sub(r'\*([^*]+)\*', r'\1', text)
|
||||||
|
# Code
|
||||||
|
text = re.sub(r'`([^`]+)`', r'\1', text)
|
||||||
|
# Links
|
||||||
|
text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
def add_formatted_text(paragraph, text):
|
||||||
|
"""Add text with basic formatting to a paragraph."""
|
||||||
|
# Split by formatting markers and add runs
|
||||||
|
parts = re.split(r'(\*\*[^*]+\*\*|`[^`]+`|\[[^\]]+\]\([^)]+\))', text)
|
||||||
|
|
||||||
|
for part in parts:
|
||||||
|
if not part:
|
||||||
|
continue
|
||||||
|
if part.startswith('**') and part.endswith('**'):
|
||||||
|
run = paragraph.add_run(part[2:-2])
|
||||||
|
run.bold = True
|
||||||
|
elif part.startswith('`') and part.endswith('`'):
|
||||||
|
run = paragraph.add_run(part[1:-1])
|
||||||
|
run.font.name = 'Consolas'
|
||||||
|
run.font.size = Pt(9)
|
||||||
|
# Add light background for inline code
|
||||||
|
run.font.highlight_color = 15 # Light gray (WD_COLOR_INDEX.GRAY_25)
|
||||||
|
elif part.startswith('[') and '](' in part:
|
||||||
|
match = re.match(r'\[([^\]]+)\]\(([^)]+)\)', part)
|
||||||
|
if match:
|
||||||
|
run = paragraph.add_run(match.group(1))
|
||||||
|
run.font.color.rgb = RGBColor(0, 0, 255)
|
||||||
|
run.underline = True
|
||||||
|
else:
|
||||||
|
paragraph.add_run(part)
|
||||||
|
|
||||||
|
def convert_md_to_docx(md_file, docx_file):
|
||||||
|
"""Convert a markdown file to a Word document."""
|
||||||
|
print(f"Converting {md_file} to {docx_file}...")
|
||||||
|
|
||||||
|
with open(md_file, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
elements = parse_markdown(content)
|
||||||
|
|
||||||
|
doc = Document()
|
||||||
|
|
||||||
|
# Set default font
|
||||||
|
style = doc.styles['Normal']
|
||||||
|
style.font.name = 'Calibri'
|
||||||
|
style.font.size = Pt(11)
|
||||||
|
|
||||||
|
for elem_type, elem_content in elements:
|
||||||
|
if elem_type == 'h1':
|
||||||
|
p = doc.add_heading(clean_text(elem_content), level=0)
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
|
||||||
|
elif elem_type == 'h2':
|
||||||
|
doc.add_heading(clean_text(elem_content), level=1)
|
||||||
|
|
||||||
|
elif elem_type == 'h3':
|
||||||
|
doc.add_heading(clean_text(elem_content), level=2)
|
||||||
|
|
||||||
|
elif elem_type == 'h4':
|
||||||
|
doc.add_heading(clean_text(elem_content), level=3)
|
||||||
|
|
||||||
|
elif elem_type == 'hr':
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
p.add_run('─' * 70)
|
||||||
|
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||||
|
|
||||||
|
elif elem_type == 'para':
|
||||||
|
p = doc.add_paragraph()
|
||||||
|
add_formatted_text(p, elem_content)
|
||||||
|
|
||||||
|
elif elem_type == 'code':
|
||||||
|
code_lang, code_text = elem_content
|
||||||
|
add_code_block(doc, code_text, code_lang)
|
||||||
|
|
||||||
|
elif elem_type == 'bullet':
|
||||||
|
for item in elem_content:
|
||||||
|
p = doc.add_paragraph(style='List Bullet')
|
||||||
|
add_formatted_text(p, item)
|
||||||
|
|
||||||
|
elif elem_type == 'numbered':
|
||||||
|
for item in elem_content:
|
||||||
|
p = doc.add_paragraph(style='List Number')
|
||||||
|
add_formatted_text(p, item)
|
||||||
|
|
||||||
|
elif elem_type == 'table':
|
||||||
|
# Parse table
|
||||||
|
rows = []
|
||||||
|
for line in elem_content:
|
||||||
|
if '---' in line:
|
||||||
|
continue
|
||||||
|
cells = [c.strip() for c in line.split('|')[1:-1]]
|
||||||
|
if cells:
|
||||||
|
rows.append(cells)
|
||||||
|
|
||||||
|
if rows:
|
||||||
|
num_cols = len(rows[0])
|
||||||
|
table = doc.add_table(rows=len(rows), cols=num_cols)
|
||||||
|
table.style = 'Table Grid'
|
||||||
|
table.alignment = WD_TABLE_ALIGNMENT.CENTER
|
||||||
|
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
for j, cell in enumerate(row):
|
||||||
|
if j < num_cols:
|
||||||
|
table.rows[i].cells[j].text = clean_text(cell)
|
||||||
|
# Bold and shade header row
|
||||||
|
if i == 0:
|
||||||
|
set_cell_shading(table.rows[i].cells[j], "E0E0E0")
|
||||||
|
for para in table.rows[i].cells[j].paragraphs:
|
||||||
|
for run in para.runs:
|
||||||
|
run.bold = True
|
||||||
|
|
||||||
|
# Add spacing after table
|
||||||
|
doc.add_paragraph()
|
||||||
|
|
||||||
|
doc.save(docx_file)
|
||||||
|
print(f" Created: {docx_file}")
|
||||||
|
|
||||||
|
def main():
|
||||||
|
docs_dir = '/home/camp/projects/powershell/docs'
|
||||||
|
|
||||||
|
md_files = [
|
||||||
|
'Update-ShopfloorPCs-Remote.md',
|
||||||
|
'Invoke-RemoteMaintenance.md',
|
||||||
|
'Update-PC-CompleteAsset.md',
|
||||||
|
'DATA_COLLECTION_PARITY.md'
|
||||||
|
]
|
||||||
|
|
||||||
|
for md_file in md_files:
|
||||||
|
md_path = os.path.join(docs_dir, md_file)
|
||||||
|
docx_path = os.path.join(docs_dir, md_file.replace('.md', '.docx'))
|
||||||
|
|
||||||
|
if os.path.exists(md_path):
|
||||||
|
convert_md_to_docx(md_path, docx_path)
|
||||||
|
else:
|
||||||
|
print(f"Warning: {md_path} not found")
|
||||||
|
|
||||||
|
print("\nConversion complete!")
|
||||||
|
print(f"Word documents saved to: {docs_dir}")
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
2
edncfix
2
edncfix
Submodule edncfix updated: 28641c47c5...2748bfa037
1149
fixnetworkshare/NetworkDriveManager.ps1
Normal file
1149
fixnetworkshare/NetworkDriveManager.ps1
Normal file
File diff suppressed because it is too large
Load Diff
@@ -160,9 +160,9 @@ try {
|
|||||||
# Machine numbers that indicate specific PC types (these ARE valid machine numbers)
|
# Machine numbers that indicate specific PC types (these ARE valid machine numbers)
|
||||||
$machineTypeIndicators = @{
|
$machineTypeIndicators = @{
|
||||||
"^0?600$" = "Wax Trace" # Wax trace machines
|
"^0?600$" = "Wax Trace" # Wax trace machines
|
||||||
"^0?(612|613|615)$" = "Part Marker" # Part marker machines
|
"^0?(612|613|615)$" = "Inspection" # Part marker machines
|
||||||
"^M?(612|613|615)$" = "Part Marker" # Part marker machines (M prefix)
|
"^M?(612|613|615)$" = "Inspection" # Part marker machines (M prefix)
|
||||||
"^8003$" = "Part Marker" # Part marker machines
|
"^8003$" = "Inspection" # Part marker machines
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check if machine number indicates a specific PC type
|
# Check if machine number indicates a specific PC type
|
||||||
|
|||||||
29
remote-execution/DeployOpenTextProfiles-Examples.txt
Normal file
29
remote-execution/DeployOpenTextProfiles-Examples.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# DeployOpenTextProfiles - Example Usage
|
||||||
|
# Source: \\tsgwp00525.wjs.geaerospace.net\shared\dt\csf\
|
||||||
|
|
||||||
|
# Single PC
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF" -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# Multiple PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -ComputerName "G1ZTNCX3ESF","G1ZTNCX4ESF","G1ZTNCX5ESF" -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# All shopfloor PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType Shopfloor -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# Wax / Trace PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType "Wax / Trace" -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# Keyence PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType Keyence -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# Genspect PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType Genspect -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# Heat Treat PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType "Heat Treat" -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# CMM PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType CMM -Task DeployOpenTextProfiles
|
||||||
|
|
||||||
|
# Inspection PCs
|
||||||
|
.\Invoke-RemoteMaintenance.ps1 -PcType Inspection -Task DeployOpenTextProfiles
|
||||||
@@ -13,8 +13,11 @@ REQUIREMENTS
|
|||||||
|
|
||||||
- PowerShell 5.1+
|
- PowerShell 5.1+
|
||||||
- Run as Administrator (required for scheduling only)
|
- Run as Administrator (required for scheduling only)
|
||||||
- Invoke-RemoteMaintenance.ps1 in the same folder
|
- ALL scripts must be in the SAME folder:
|
||||||
- A PC list text file (one hostname per line)
|
Invoke-RemoteMaintenance.ps1
|
||||||
|
Schedule-Maintenance.ps1
|
||||||
|
Export-PCList.ps1
|
||||||
|
shopfloor-pcs.txt (or your PC list file)
|
||||||
|
|
||||||
|
|
||||||
============================================================
|
============================================================
|
||||||
@@ -34,53 +37,92 @@ PARAMETERS
|
|||||||
|
|
||||||
|
|
||||||
============================================================
|
============================================================
|
||||||
USAGE
|
STEP 1: Save Credentials (one time)
|
||||||
============================================================
|
============================================================
|
||||||
|
|
||||||
1. SAVE CREDENTIALS (one time, does not require admin)
|
.\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd"
|
||||||
|
|
||||||
.\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\570005354" -Password "MyP@ssw0rd"
|
- Encrypted with AES-256 key
|
||||||
|
- Works from normal or admin PowerShell
|
||||||
- Encrypted with Windows DPAPI
|
- Stored in .creds\ folder (not plaintext)
|
||||||
- Only your user account on this machine can decrypt
|
- Re-run if your password changes
|
||||||
- Re-run if your password changes
|
|
||||||
|
|
||||||
|
|
||||||
2. RUN IMMEDIATELY (does not require admin)
|
|
||||||
|
|
||||||
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
|
|
||||||
|
|
||||||
|
|
||||||
3. SCHEDULE A ONE-TIME TASK (requires admin)
|
|
||||||
|
|
||||||
# Reboot one PC today at 3:00 PM
|
|
||||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\test-reboot.txt" -Task Reboot -TaskFrequency Once -TaskTime "15:00" -TaskDate "2026-02-19"
|
|
||||||
|
|
||||||
# Reboot all PCs Sunday Feb 22 at 12:01 AM
|
|
||||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22"
|
|
||||||
|
|
||||||
|
|
||||||
4. SCHEDULE A RECURRING TASK (requires admin)
|
|
||||||
|
|
||||||
# Every Sunday at 12:01 AM
|
|
||||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01"
|
|
||||||
|
|
||||||
# Every day at 2:00 AM
|
|
||||||
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task DiskCleanup -TaskFrequency Daily -TaskTime "02:00"
|
|
||||||
|
|
||||||
|
|
||||||
5. MANAGE SCHEDULED TASKS
|
|
||||||
|
|
||||||
Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" }
|
|
||||||
Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
|
|
||||||
Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
|
|
||||||
|
|
||||||
|
|
||||||
============================================================
|
============================================================
|
||||||
LOGS
|
STEP 2: Generate PC List
|
||||||
============================================================
|
============================================================
|
||||||
|
|
||||||
.\logs\maintenance-YYYY-MM-DD_HHMMSS-TaskName.log
|
# All shopfloor PCs from API
|
||||||
|
.\Export-PCList.ps1
|
||||||
|
|
||||||
|
# Filter by type
|
||||||
|
.\Export-PCList.ps1 -PcType Shopfloor
|
||||||
|
|
||||||
|
# Single PC for testing
|
||||||
|
"G63TVG04ESF" | Out-File -FilePath ".\test-reboot.txt" -Encoding UTF8
|
||||||
|
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
STEP 3: Run or Schedule
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
RUN IMMEDIATELY (no admin needed):
|
||||||
|
.\Schedule-Maintenance.ps1 -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot
|
||||||
|
|
||||||
|
SCHEDULE ONE-TIME (admin required):
|
||||||
|
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Once -TaskTime "00:01" -TaskDate "2026-02-22"
|
||||||
|
|
||||||
|
SCHEDULE RECURRING (admin required):
|
||||||
|
.\Schedule-Maintenance.ps1 -CreateScheduledTask -ComputerListFile ".\shopfloor-pcs.txt" -Task Reboot -TaskFrequency Weekly -TaskDay Sunday -TaskTime "00:01"
|
||||||
|
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
CHECKING RESULTS
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
AFTER A SCHEDULED RUN:
|
||||||
|
Get-Content ".\logs\LAST-RUN-SUMMARY.txt"
|
||||||
|
|
||||||
|
FULL LOG (most recent):
|
||||||
|
Get-ChildItem ".\logs\" -Filter "maintenance-*-Reboot.log" | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content
|
||||||
|
|
||||||
|
CHECK IF TASK RAN:
|
||||||
|
Get-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" | Get-ScheduledTaskInfo
|
||||||
|
|
||||||
|
LastTaskResult = 0 means success
|
||||||
|
Anything else means it errored before writing logs
|
||||||
|
|
||||||
|
LOGS LOCATION:
|
||||||
|
.\logs\ (inside your scripts folder)
|
||||||
|
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
MANAGING SCHEDULED TASKS
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
# List maintenance tasks
|
||||||
|
Get-ScheduledTask | Where-Object { $_.TaskName -like "ShopfloorMaintenance*" }
|
||||||
|
|
||||||
|
# Run now (don't wait for schedule)
|
||||||
|
Start-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
|
||||||
|
|
||||||
|
# Delete a task
|
||||||
|
Unregister-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot"
|
||||||
|
|
||||||
|
# Or use: Task Scheduler GUI (taskschd.msc)
|
||||||
|
|
||||||
|
|
||||||
|
============================================================
|
||||||
|
AVAILABLE TASKS
|
||||||
|
============================================================
|
||||||
|
|
||||||
|
Reboot, DISM, SFC, OptimizeDisk, DiskCleanup,
|
||||||
|
ClearUpdateCache, ClearBrowserCache, RestartSpooler,
|
||||||
|
FlushDNS, RestartWinRM, SetTimezone, SyncTime,
|
||||||
|
UpdateEMxAuthToken, DeployUDCWebServerConfig,
|
||||||
|
UpdateDNCMXHosts,
|
||||||
|
InstallDashboard, InstallLobbyDisplay,
|
||||||
|
UninstallDashboard, UninstallLobbyDisplay
|
||||||
|
|
||||||
|
|
||||||
============================================================
|
============================================================
|
||||||
@@ -88,13 +130,24 @@ TROUBLESHOOTING
|
|||||||
============================================================
|
============================================================
|
||||||
|
|
||||||
"No saved credentials found"
|
"No saved credentials found"
|
||||||
-> Run -SaveCredential with -Username and -Password
|
-> .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\user" -Password "pass"
|
||||||
|
|
||||||
"Access is denied" when scheduling
|
"Access is denied" when scheduling
|
||||||
-> Right-click PowerShell -> Run as Administrator
|
-> Right-click PowerShell -> Run as Administrator
|
||||||
|
|
||||||
"No credentials provided. Exiting."
|
"No credentials provided. Exiting."
|
||||||
-> GUI prompt failed. Use -Username and -Password flags
|
-> Use -Username and -Password flags instead of GUI prompt
|
||||||
|
|
||||||
|
No logs folder / empty logs
|
||||||
|
-> Task may not have run yet. Check:
|
||||||
|
Get-ScheduledTask -TaskName "ShopfloorMaintenance-Reboot" | Get-ScheduledTaskInfo
|
||||||
|
|
||||||
Password changed
|
Password changed
|
||||||
-> Re-run -SaveCredential with new password
|
-> .\Schedule-Maintenance.ps1 -SaveCredential -Username "DS\user" -Password "newpass"
|
||||||
|
|
||||||
|
NOTE ABOUT "Running as AEROAD\SSO":
|
||||||
|
This is normal. The scheduled task runs as your Windows login.
|
||||||
|
It still uses your SAVED credentials for WinRM connections
|
||||||
|
to the remote shopfloor PCs. Two separate accounts:
|
||||||
|
1. Your Windows login = runs the script
|
||||||
|
2. Saved credentials = connects to remote PCs
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
18
remote-execution/Resume-Download.bat
Normal file
18
remote-execution/Resume-Download.bat
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
@echo off
|
||||||
|
:: Resume-Download — Resumable download for SharePoint and general URLs
|
||||||
|
:: Uses domain credentials for SharePoint auth
|
||||||
|
|
||||||
|
if "%~1"=="" (
|
||||||
|
set /p "URL=Enter download URL: "
|
||||||
|
) else (
|
||||||
|
set "URL=%~1"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%~2"=="" (
|
||||||
|
set /p "DEST=Save as (full path, e.g. C:\Temp\file.iso): "
|
||||||
|
) else (
|
||||||
|
set "DEST=%~2"
|
||||||
|
)
|
||||||
|
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Resume-Download.ps1" -Url "%URL%" -Destination "%DEST%"
|
||||||
|
pause
|
||||||
117
remote-execution/Resume-Download.ps1
Normal file
117
remote-execution/Resume-Download.ps1
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
param(
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$Url,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[string]$Destination
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
|
||||||
|
Write-Host "============================================"
|
||||||
|
Write-Host " Resumable File Download"
|
||||||
|
Write-Host "============================================"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Destination: $Destination"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Check for partial file to resume
|
||||||
|
$startBytes = 0
|
||||||
|
if (Test-Path $Destination) {
|
||||||
|
$startBytes = (Get-Item $Destination).Length
|
||||||
|
if ($startBytes -gt 0) {
|
||||||
|
Write-Host "Partial file found: $([math]::Round($startBytes / 1MB, 1)) MB already downloaded"
|
||||||
|
Write-Host "Resuming..."
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensure destination directory exists
|
||||||
|
$destDir = Split-Path $Destination -Parent
|
||||||
|
if ($destDir -and !(Test-Path $destDir)) {
|
||||||
|
New-Item -ItemType Directory -Path $destDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build headers
|
||||||
|
$headers = @{}
|
||||||
|
if ($startBytes -gt 0) {
|
||||||
|
$headers["Range"] = "bytes=$startBytes-"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use Invoke-WebRequest with domain credentials for SharePoint auth
|
||||||
|
$params = @{
|
||||||
|
Uri = $Url
|
||||||
|
OutFile = $Destination
|
||||||
|
UseDefaultCredentials = $true
|
||||||
|
UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
||||||
|
Headers = $headers
|
||||||
|
UseBasicParsing = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# If resuming, we need to handle appending manually
|
||||||
|
if ($startBytes -gt 0) {
|
||||||
|
# Download to a temp file, then append
|
||||||
|
$tempFile = "$Destination.partial"
|
||||||
|
$params.OutFile = $tempFile
|
||||||
|
|
||||||
|
Write-Host "Downloading..."
|
||||||
|
Invoke-WebRequest @params
|
||||||
|
|
||||||
|
# Check if we got actual content
|
||||||
|
$tempSize = (Get-Item $tempFile).Length
|
||||||
|
if ($tempSize -eq 0) {
|
||||||
|
Remove-Item $tempFile -Force
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "WARNING: Server returned 0 bytes. The URL may have expired."
|
||||||
|
Write-Host "Get a fresh SharePoint link and try again."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Append to existing file
|
||||||
|
$existingBytes = [System.IO.File]::ReadAllBytes($Destination)
|
||||||
|
$newBytes = [System.IO.File]::ReadAllBytes($tempFile)
|
||||||
|
$combined = New-Object byte[] ($existingBytes.Length + $newBytes.Length)
|
||||||
|
[System.Buffer]::BlockCopy($existingBytes, 0, $combined, 0, $existingBytes.Length)
|
||||||
|
[System.Buffer]::BlockCopy($newBytes, 0, $combined, $existingBytes.Length, $newBytes.Length)
|
||||||
|
[System.IO.File]::WriteAllBytes($Destination, $combined)
|
||||||
|
Remove-Item $tempFile -Force
|
||||||
|
|
||||||
|
$totalMB = [math]::Round(($existingBytes.Length + $newBytes.Length) / 1MB, 1)
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Download complete: $totalMB MB saved to $Destination"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Downloading..."
|
||||||
|
Invoke-WebRequest @params
|
||||||
|
|
||||||
|
# Verify we got actual content
|
||||||
|
$fileSize = (Get-Item $Destination).Length
|
||||||
|
if ($fileSize -eq 0) {
|
||||||
|
Remove-Item $Destination -Force
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "WARNING: Downloaded 0 bytes. Possible causes:"
|
||||||
|
Write-Host " - SharePoint URL expired or requires browser login"
|
||||||
|
Write-Host " - URL is a redirect/login page, not the actual file"
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Try this: In Edge, start the download, then go to"
|
||||||
|
Write-Host "edge://downloads and copy the source URL from there."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$totalMB = [math]::Round($fileSize / 1MB, 1)
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Download complete: $totalMB MB saved to $Destination"
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Error: $_"
|
||||||
|
Write-Host ""
|
||||||
|
if (Test-Path $Destination) {
|
||||||
|
$partialMB = [math]::Round((Get-Item $Destination).Length / 1MB, 1)
|
||||||
|
Write-Host "Partial file kept: $partialMB MB"
|
||||||
|
}
|
||||||
|
Write-Host "Re-run with the same arguments to resume."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
32
remote-execution/Run-UpdateDNCMXHosts.bat
Normal file
32
remote-execution/Run-UpdateDNCMXHosts.bat
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
@echo off
|
||||||
|
REM Run-UpdateDNCMXHosts.bat
|
||||||
|
REM Updates FtpHostPrimary/Secondary in DNC\MX registry on remote shopfloor PCs
|
||||||
|
REM Usage:
|
||||||
|
REM Run-UpdateDNCMXHosts.bat -> runs on all PCs in shopfloor-pcs.txt
|
||||||
|
REM Run-UpdateDNCMXHosts.bat G5N9PWM3ESF -> runs on a single PC (for testing)
|
||||||
|
|
||||||
|
echo ============================================================
|
||||||
|
echo UpdateDNCMXHosts - FTP Host Migration
|
||||||
|
echo tsgwp00525.us.ae.ge.com -^> tsgwp00525.wjs.geaerospace.net
|
||||||
|
echo ============================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
if not "%~1"=="" (
|
||||||
|
echo Target: %~1
|
||||||
|
echo.
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -File "%~dp0Invoke-RemoteMaintenance.ps1" -ComputerName "%~1" -Task UpdateDNCMXHosts -LogFile
|
||||||
|
) else (
|
||||||
|
if not exist "%~dp0shopfloor-pcs.txt" (
|
||||||
|
echo ERROR: shopfloor-pcs.txt not found in %~dp0
|
||||||
|
echo Either place shopfloor-pcs.txt in the same folder or pass a PC name:
|
||||||
|
echo Run-UpdateDNCMXHosts.bat G5N9PWM3ESF
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo Target: shopfloor-pcs.txt
|
||||||
|
echo.
|
||||||
|
powershell.exe -ExecutionPolicy Bypass -File "%~dp0Invoke-RemoteMaintenance.ps1" -ComputerListFile "%~dp0shopfloor-pcs.txt" -Task UpdateDNCMXHosts -LogFile
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
@@ -68,11 +68,41 @@ param(
|
|||||||
[string]$Password
|
[string]$Password
|
||||||
)
|
)
|
||||||
|
|
||||||
$credFile = Join-Path $PSScriptRoot ".maintenance-cred.xml"
|
$credDir = Join-Path $PSScriptRoot ".creds"
|
||||||
|
$keyFile = Join-Path $credDir "aes.key"
|
||||||
|
$userFile = Join-Path $credDir "username.txt"
|
||||||
|
$passFile = Join-Path $credDir "password.txt"
|
||||||
$scriptDir = $PSScriptRoot
|
$scriptDir = $PSScriptRoot
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Save credentials (DPAPI - only decryptable by this user on this machine)
|
# Helper: load or create AES key (works across all user contexts on this PC)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
function Get-AESKey {
|
||||||
|
if (-not (Test-Path $credDir)) {
|
||||||
|
New-Item -Path $credDir -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
if (Test-Path $keyFile) {
|
||||||
|
return [byte[]](Get-Content $keyFile)
|
||||||
|
}
|
||||||
|
$key = New-Object byte[] 32
|
||||||
|
[System.Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($key)
|
||||||
|
$key | Set-Content $keyFile
|
||||||
|
return $key
|
||||||
|
}
|
||||||
|
|
||||||
|
function Load-SavedCredential {
|
||||||
|
if (-not (Test-Path $userFile) -or -not (Test-Path $passFile)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$key = Get-AESKey
|
||||||
|
$user = Get-Content $userFile
|
||||||
|
$encPass = Get-Content $passFile
|
||||||
|
$secPass = $encPass | ConvertTo-SecureString -Key $key
|
||||||
|
return New-Object System.Management.Automation.PSCredential($user, $secPass)
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Save credentials (AES key file - works from any user context on this PC)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
if ($SaveCredential) {
|
if ($SaveCredential) {
|
||||||
if ($Username -and $Password) {
|
if ($Username -and $Password) {
|
||||||
@@ -87,9 +117,11 @@ if ($SaveCredential) {
|
|||||||
Write-Host "No credentials provided. Exiting." -ForegroundColor Red
|
Write-Host "No credentials provided. Exiting." -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
$cred | Export-Clixml -Path $credFile
|
$key = Get-AESKey
|
||||||
Write-Host "Credentials saved to: $credFile" -ForegroundColor Green
|
$cred.UserName | Set-Content $userFile
|
||||||
Write-Host "Encrypted with DPAPI - only YOUR user account on THIS machine can decrypt them." -ForegroundColor Yellow
|
$cred.Password | ConvertFrom-SecureString -Key $key | Set-Content $passFile
|
||||||
|
Write-Host "Credentials saved to: $credDir" -ForegroundColor Green
|
||||||
|
Write-Host "Encrypted with AES-256 key - works from any user context (normal or admin)." -ForegroundColor Yellow
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "You can now run tasks unattended:" -ForegroundColor Cyan
|
Write-Host "You can now run tasks unattended:" -ForegroundColor Cyan
|
||||||
Write-Host " .\Schedule-Maintenance.ps1 -ComputerListFile '.\shopfloor-pcs.txt' -Task Reboot"
|
Write-Host " .\Schedule-Maintenance.ps1 -ComputerListFile '.\shopfloor-pcs.txt' -Task Reboot"
|
||||||
@@ -113,7 +145,7 @@ if ($CreateScheduledTask) {
|
|||||||
$absListFile = (Resolve-Path $ComputerListFile -ErrorAction Stop).Path
|
$absListFile = (Resolve-Path $ComputerListFile -ErrorAction Stop).Path
|
||||||
$absScript = Join-Path $scriptDir "Schedule-Maintenance.ps1"
|
$absScript = Join-Path $scriptDir "Schedule-Maintenance.ps1"
|
||||||
|
|
||||||
if (-not (Test-Path $credFile)) {
|
if (-not (Test-Path $passFile)) {
|
||||||
Write-Host "ERROR: No saved credentials found. Run with -SaveCredential first." -ForegroundColor Red
|
Write-Host "ERROR: No saved credentials found. Run with -SaveCredential first." -ForegroundColor Red
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
@@ -177,20 +209,13 @@ if (-not $ComputerListFile) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Load saved credentials
|
# Load saved credentials
|
||||||
if (-not (Test-Path $credFile)) {
|
$cred = Load-SavedCredential
|
||||||
Write-Host "ERROR: No saved credentials found at $credFile" -ForegroundColor Red
|
if (-not $cred) {
|
||||||
|
Write-Host "ERROR: No saved credentials found in $credDir" -ForegroundColor Red
|
||||||
Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow
|
Write-Host "Run with -SaveCredential first to store credentials." -ForegroundColor Yellow
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
|
Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green
|
||||||
try {
|
|
||||||
$cred = Import-Clixml -Path $credFile
|
|
||||||
Write-Host "Loaded saved credentials for: $($cred.UserName)" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Write-Host "ERROR: Failed to load credentials: $_" -ForegroundColor Red
|
|
||||||
Write-Host "Re-run with -SaveCredential to save new credentials." -ForegroundColor Yellow
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# Log output
|
# Log output
|
||||||
$logDir = Join-Path $scriptDir "logs"
|
$logDir = Join-Path $scriptDir "logs"
|
||||||
@@ -202,7 +227,41 @@ Write-Host "Log: $logFile" -ForegroundColor Gray
|
|||||||
|
|
||||||
$mainScript = Join-Path $scriptDir "Invoke-RemoteMaintenance.ps1"
|
$mainScript = Join-Path $scriptDir "Invoke-RemoteMaintenance.ps1"
|
||||||
|
|
||||||
& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred 2>&1 | Tee-Object -FilePath $logFile
|
Start-Transcript -Path $logFile -Force | Out-Null
|
||||||
|
& $mainScript -ComputerListFile $ComputerListFile -Task $Task -Credential $cred
|
||||||
|
Stop-Transcript | Out-Null
|
||||||
|
|
||||||
|
# Parse log for results summary
|
||||||
|
$logContent = Get-Content $logFile -ErrorAction SilentlyContinue
|
||||||
|
$okPCs = @($logContent | Select-String '\[OK\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value })
|
||||||
|
$failPCs = @($logContent | Select-String '\[FAIL\]\s+(\S+)' | ForEach-Object { $_.Matches[0].Groups[1].Value } | Where-Object { $_ -ne ':' } | Sort-Object -Unique)
|
||||||
|
|
||||||
|
$summaryFile = Join-Path $logDir "LAST-RUN-SUMMARY.txt"
|
||||||
|
$summary = @()
|
||||||
|
$summary += "============================================"
|
||||||
|
$summary += " MAINTENANCE RESULTS - $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||||
|
$summary += " Task: $Task"
|
||||||
|
$summary += "============================================"
|
||||||
|
$summary += ""
|
||||||
|
$summary += "SUCCEEDED: $($okPCs.Count)"
|
||||||
|
foreach ($pc in $okPCs) { $summary += " [OK] $pc" }
|
||||||
|
$summary += ""
|
||||||
|
$summary += "FAILED: $($failPCs.Count)"
|
||||||
|
foreach ($pc in $failPCs) { $summary += " [FAIL] $pc" }
|
||||||
|
$summary += ""
|
||||||
|
$summary += "Full log: $logFile"
|
||||||
|
|
||||||
|
$summary | Out-File -FilePath $summaryFile -Encoding UTF8
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green
|
Write-Host "Complete. Log saved to: $logFile" -ForegroundColor Green
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== RESULTS ===" -ForegroundColor White
|
||||||
|
Write-Host " Succeeded: $($okPCs.Count)" -ForegroundColor Green
|
||||||
|
foreach ($pc in $okPCs) { Write-Host " $pc" -ForegroundColor Green }
|
||||||
|
if ($failPCs.Count -gt 0) {
|
||||||
|
Write-Host " Failed: $($failPCs.Count)" -ForegroundColor Red
|
||||||
|
foreach ($pc in $failPCs) { Write-Host " $pc" -ForegroundColor Red }
|
||||||
|
}
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "Summary also saved to: $summaryFile" -ForegroundColor Yellow
|
||||||
|
|||||||
279
remote-execution/udc/UDC_Update.ps1
Executable file
279
remote-execution/udc/UDC_Update.ps1
Executable file
@@ -0,0 +1,279 @@
|
|||||||
|
# ============================================
|
||||||
|
# UDC Application Update Script (PowerShell)
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$Version
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set error action preference
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# Set variables
|
||||||
|
$UDC_PATH = "C:\Program Files\UDC"
|
||||||
|
$VERSION_FILE = "S:\SPC\UDC\UDC_Update.txt"
|
||||||
|
$LOG_DIR = "S:\DT\Cameron\UDC\logs"
|
||||||
|
|
||||||
|
# Get hostname and timestamp
|
||||||
|
$HOSTNAME = $env:COMPUTERNAME
|
||||||
|
$TIMESTAMP = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||||
|
$LOG_FILE = "$LOG_DIR\logs_$($HOSTNAME)_$TIMESTAMP.txt"
|
||||||
|
|
||||||
|
# Flag to track if we should skip the update
|
||||||
|
$SKIP_UPDATE = $false
|
||||||
|
|
||||||
|
# Function to write logs
|
||||||
|
function Write-Log {
|
||||||
|
param([string]$Message)
|
||||||
|
$LogMessage = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
|
||||||
|
Write-Host $Message
|
||||||
|
Add-Content -Path $LOG_FILE -Value $LogMessage -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
# Create log directory if needed
|
||||||
|
try {
|
||||||
|
if (-not (Test-Path $LOG_DIR)) {
|
||||||
|
New-Item -Path $LOG_DIR -ItemType Directory -Force | Out-Null
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host "WARNING: Cannot access log directory $LOG_DIR"
|
||||||
|
Write-Host "Update will continue but logging is disabled."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "============================================"
|
||||||
|
Write-Log "UDC Update Script Started on $HOSTNAME"
|
||||||
|
Write-Log "============================================"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 1: Check if PPMon.exe or ppdcs.exe are running
|
||||||
|
# ============================================
|
||||||
|
Write-Host "Checking for conflicting processes..."
|
||||||
|
$conflictingProcesses = Get-Process -Name "PPMon", "ppdcs" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($conflictingProcesses) {
|
||||||
|
foreach ($proc in $conflictingProcesses) {
|
||||||
|
Write-Log "$($proc.Name).exe is running. Skipping update process."
|
||||||
|
Write-Host "$($proc.Name).exe is running. Update will be skipped."
|
||||||
|
}
|
||||||
|
$SKIP_UPDATE = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 2: Check if UDC directory exists
|
||||||
|
# ============================================
|
||||||
|
Write-Host "Checking if UDC is installed..."
|
||||||
|
if (-not (Test-Path $UDC_PATH)) {
|
||||||
|
Write-Log "UDC directory not found. Exiting."
|
||||||
|
Write-Host "UDC is not installed on this machine."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 3: Check if UDC.exe exists
|
||||||
|
# ============================================
|
||||||
|
$UDC_EXE = "$UDC_PATH\UDC.exe"
|
||||||
|
if (-not (Test-Path $UDC_EXE)) {
|
||||||
|
Write-Log "UDC.exe not found. Exiting."
|
||||||
|
Write-Host "UDC.exe not found in installation directory."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# ONLY PROCEED WITH UPDATE IF NOT SKIPPED
|
||||||
|
# ============================================
|
||||||
|
if (-not $SKIP_UPDATE) {
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 4: Determine target version
|
||||||
|
# ============================================
|
||||||
|
if ($Version) {
|
||||||
|
$NETWORK_VERSION = $Version
|
||||||
|
Write-Host "Using override version: $NETWORK_VERSION"
|
||||||
|
Write-Log "Override version specified: $NETWORK_VERSION"
|
||||||
|
} else {
|
||||||
|
if (-not (Test-Path $VERSION_FILE)) {
|
||||||
|
Write-Log "Version file not found: $VERSION_FILE"
|
||||||
|
Write-Host "ERROR: Version file not found."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Reading version information..."
|
||||||
|
try {
|
||||||
|
$versionContent = Get-Content $VERSION_FILE | Where-Object { $_ -match "Version:" }
|
||||||
|
$NETWORK_VERSION = ($versionContent -split ":")[1].Trim()
|
||||||
|
|
||||||
|
if ([string]::IsNullOrEmpty($NETWORK_VERSION)) {
|
||||||
|
throw "Could not parse network version"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Network version: $NETWORK_VERSION"
|
||||||
|
Write-Log "Network version: $NETWORK_VERSION"
|
||||||
|
} catch {
|
||||||
|
Write-Log "Could not read network version. Error: $_"
|
||||||
|
Write-Host "ERROR: Could not determine network version."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build source path from version
|
||||||
|
$SOURCE_PATH = "S:\SPC\UDC\UDC_$NETWORK_VERSION"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 5: Check if source files exist
|
||||||
|
# ============================================
|
||||||
|
if (-not (Test-Path $SOURCE_PATH)) {
|
||||||
|
Write-Log "Source path not found: $SOURCE_PATH"
|
||||||
|
Write-Host "ERROR: Update source files not found at $SOURCE_PATH"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 6: Get local UDC.exe version
|
||||||
|
# ============================================
|
||||||
|
try {
|
||||||
|
$fileVersion = (Get-Item $UDC_EXE).VersionInfo.FileVersion
|
||||||
|
# Trim to 3 parts if it has 4 (e.g., 1.0.30.0 -> 1.0.30)
|
||||||
|
$versionParts = $fileVersion.Split('.')
|
||||||
|
if ($versionParts.Count -eq 4) {
|
||||||
|
$LOCAL_VERSION = "$($versionParts[0]).$($versionParts[1]).$($versionParts[2])"
|
||||||
|
} else {
|
||||||
|
$LOCAL_VERSION = $fileVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Local version: $LOCAL_VERSION"
|
||||||
|
Write-Log "Local version: $LOCAL_VERSION"
|
||||||
|
} catch {
|
||||||
|
Write-Log "Could not read local version. Error: $_"
|
||||||
|
Write-Host "ERROR: Could not determine local version."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 7: Compare versions
|
||||||
|
# ============================================
|
||||||
|
try {
|
||||||
|
$netVer = [version]$NETWORK_VERSION
|
||||||
|
$localVer = [version]$LOCAL_VERSION
|
||||||
|
|
||||||
|
if ($netVer -eq $localVer) {
|
||||||
|
Write-Log "Versions match ($LOCAL_VERSION). No update needed."
|
||||||
|
Write-Host "Version is current. No update required."
|
||||||
|
$SKIP_UPDATE = $true
|
||||||
|
}
|
||||||
|
elseif ($netVer -le $localVer) {
|
||||||
|
Write-Log "Network version ($NETWORK_VERSION) not newer than local ($LOCAL_VERSION)."
|
||||||
|
Write-Host "Local version is current or newer. No update required."
|
||||||
|
$SKIP_UPDATE = $true
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Host "Update required: $LOCAL_VERSION -> $NETWORK_VERSION"
|
||||||
|
Write-Log "Update required: $LOCAL_VERSION -> $NETWORK_VERSION"
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Log "Error comparing versions. Error: $_"
|
||||||
|
Write-Host "ERROR: Could not compare versions."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 8: Perform update if needed
|
||||||
|
# ============================================
|
||||||
|
if (-not $SKIP_UPDATE) {
|
||||||
|
|
||||||
|
# Kill UDC.exe if running
|
||||||
|
Write-Host "Checking if UDC.exe is running..."
|
||||||
|
$udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($udcProcess) {
|
||||||
|
Write-Host "UDC.exe is running. Stopping process..."
|
||||||
|
Write-Log "Killing UDC.exe"
|
||||||
|
try {
|
||||||
|
Stop-Process -Name "UDC" -Force
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
Write-Log "UDC.exe stopped successfully"
|
||||||
|
} catch {
|
||||||
|
Write-Log "Warning: Could not stop UDC.exe. Error: $_"
|
||||||
|
Write-Host "WARNING: Could not stop UDC.exe"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Copy update files
|
||||||
|
Write-Host "Copying update files..."
|
||||||
|
Write-Log "Copying files from $SOURCE_PATH to $UDC_PATH"
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Use robocopy for better performance and logging
|
||||||
|
$robocopyArgs = @(
|
||||||
|
$SOURCE_PATH,
|
||||||
|
$UDC_PATH,
|
||||||
|
"/E", # Copy subdirectories including empty ones
|
||||||
|
"/NFL", # No file list
|
||||||
|
"/NDL", # No directory list
|
||||||
|
"/NJH", # No job header
|
||||||
|
"/NJS", # No job summary
|
||||||
|
"/NC", # No class
|
||||||
|
"/NS", # No size
|
||||||
|
"/NP" # No progress
|
||||||
|
)
|
||||||
|
|
||||||
|
$result = robocopy @robocopyArgs
|
||||||
|
|
||||||
|
# Robocopy exit codes: 0-7 are success, 8+ are errors
|
||||||
|
if ($LASTEXITCODE -ge 8) {
|
||||||
|
throw "Robocopy failed with exit code $LASTEXITCODE"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "Files copied successfully."
|
||||||
|
Write-Log "Files copied successfully."
|
||||||
|
} catch {
|
||||||
|
Write-Log "ERROR: File copy failed. Error: $_"
|
||||||
|
Write-Host "ERROR: Failed to copy update files."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# Step 9: Ensure UDC.exe is running (ONLY if no conflicting processes)
|
||||||
|
# ============================================
|
||||||
|
# Re-check for conflicting processes before starting UDC
|
||||||
|
$conflictingProcesses = Get-Process -Name "PPMon", "ppdcs" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($conflictingProcesses) {
|
||||||
|
Write-Log "Conflicting processes still running. Will not start UDC.exe"
|
||||||
|
Write-Host "Conflicting processes detected. UDC.exe will not be started."
|
||||||
|
} else {
|
||||||
|
Write-Host "Verifying UDC.exe is running..."
|
||||||
|
$udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if (-not $udcProcess) {
|
||||||
|
Write-Host "UDC.exe is not running. Starting it now..."
|
||||||
|
Write-Log "UDC.exe not running. Starting UDC.exe"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Start-Process -FilePath $UDC_EXE
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
|
||||||
|
# Verify it started
|
||||||
|
$udcProcess = Get-Process -Name "UDC" -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($udcProcess) {
|
||||||
|
Write-Log "UDC.exe started successfully."
|
||||||
|
Write-Host "UDC.exe started successfully!"
|
||||||
|
} else {
|
||||||
|
Write-Log "WARNING: UDC.exe may not have started."
|
||||||
|
Write-Host "WARNING: UDC.exe may not have started properly."
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Log "WARNING: Could not start UDC.exe. Error: $_"
|
||||||
|
Write-Host "WARNING: Could not start UDC.exe"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Write-Log "UDC.exe is already running."
|
||||||
|
Write-Host "UDC.exe is already running."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "UDC Update Script Completed"
|
||||||
|
Write-Log "============================================"
|
||||||
|
exit 0
|
||||||
8
remote-execution/udc/udc_update.bat
Executable file
8
remote-execution/udc/udc_update.bat
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
@echo off
|
||||||
|
:: ============================================
|
||||||
|
:: UDC Update PowerShell Launcher
|
||||||
|
:: ============================================
|
||||||
|
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File ""U:/Scripts/UDC/UDC_Update.ps1""' -Verb RunAs}"
|
||||||
|
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
11
remote-execution/udc/udc_update_override.bat
Normal file
11
remote-execution/udc/udc_update_override.bat
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@echo off
|
||||||
|
:: ============================================
|
||||||
|
:: UDC Update Override Launcher
|
||||||
|
:: Edit the version below to force a specific update
|
||||||
|
:: ============================================
|
||||||
|
|
||||||
|
set "UDC_VERSION=1.0.32"
|
||||||
|
|
||||||
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& 'U:/Scripts/UDC/UDC_Update.ps1' -Version '%UDC_VERSION%'"
|
||||||
|
|
||||||
|
exit /b %ERRORLEVEL%
|
||||||
535
winrm-setup-package/Invoke-RemoteTask.ps1
Normal file
535
winrm-setup-package/Invoke-RemoteTask.ps1
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Simple remote maintenance toolkit for Windows PCs via WinRM.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Executes maintenance tasks on remote Windows PCs using WinRM.
|
||||||
|
Reads target computers from a text file (one hostname/IP per line).
|
||||||
|
|
||||||
|
.PARAMETER HostsFile
|
||||||
|
Path to text file containing computer names/IPs (one per line).
|
||||||
|
Lines starting with # are treated as comments.
|
||||||
|
Default: .\hosts.txt
|
||||||
|
|
||||||
|
.PARAMETER ComputerName
|
||||||
|
Single computer name or IP address (alternative to HostsFile).
|
||||||
|
|
||||||
|
.PARAMETER Task
|
||||||
|
Maintenance task to execute. Available tasks:
|
||||||
|
- RestartSpooler : Restart Print Spooler service
|
||||||
|
- FlushDNS : Clear DNS resolver cache
|
||||||
|
- ClearTempFiles : Clear Windows temp files
|
||||||
|
- DiskCleanup : Run Windows Disk Cleanup
|
||||||
|
- OptimizeDisk : TRIM (SSD) or Defrag (HDD)
|
||||||
|
- SyncTime : Force time sync with domain controller
|
||||||
|
- RestartService : Restart a specific Windows service
|
||||||
|
- RunCommand : Run a custom command
|
||||||
|
- RestartComputer : Restart the remote PC (requires confirmation)
|
||||||
|
|
||||||
|
.PARAMETER ServiceName
|
||||||
|
Service name for RestartService task.
|
||||||
|
|
||||||
|
.PARAMETER Command
|
||||||
|
Custom command for RunCommand task.
|
||||||
|
|
||||||
|
.PARAMETER Credential
|
||||||
|
PSCredential for remote authentication. Prompts if not provided.
|
||||||
|
|
||||||
|
.PARAMETER DnsSuffix
|
||||||
|
DNS suffix to append to hostnames (if not already FQDN).
|
||||||
|
Default: logon.ds.ge.com
|
||||||
|
|
||||||
|
.PARAMETER ThrottleLimit
|
||||||
|
Maximum number of concurrent remote connections.
|
||||||
|
Default: 10
|
||||||
|
|
||||||
|
.PARAMETER LogResults
|
||||||
|
Save results to a timestamped log file in the script directory.
|
||||||
|
Log files are saved as: RemoteTask_YYYYMMDD_HHMMSS.log
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Restart print spooler on all hosts in hosts.txt
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Flush DNS on a single computer
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task FlushDNS
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Run disk cleanup on hosts from custom file
|
||||||
|
.\Invoke-RemoteTask.ps1 -HostsFile ".\shopfloor-pcs.txt" -Task DiskCleanup
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Restart a specific service
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName "Spooler"
|
||||||
|
|
||||||
|
.EXAMPLE
|
||||||
|
# Run custom command
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RunCommand -Command "Get-Process | Select -First 5"
|
||||||
|
|
||||||
|
.NOTES
|
||||||
|
Author: Shop Floor Tools
|
||||||
|
Requirements: PowerShell 5.1+, WinRM enabled on targets
|
||||||
|
#>
|
||||||
|
|
||||||
|
[CmdletBinding(DefaultParameterSetName='ByFile')]
|
||||||
|
param(
|
||||||
|
[Parameter(ParameterSetName='ByFile')]
|
||||||
|
[string]$HostsFile = ".\hosts.txt",
|
||||||
|
|
||||||
|
[Parameter(ParameterSetName='ByName')]
|
||||||
|
[string[]]$ComputerName,
|
||||||
|
|
||||||
|
[Parameter(Mandatory=$true)]
|
||||||
|
[ValidateSet(
|
||||||
|
'RestartSpooler', 'FlushDNS', 'ClearTempFiles', 'DiskCleanup',
|
||||||
|
'OptimizeDisk', 'SyncTime', 'RestartService', 'RunCommand',
|
||||||
|
'GetDiskSpace', 'GetUptime', 'TestConnection', 'RestartComputer'
|
||||||
|
)]
|
||||||
|
[string]$Task,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$ServiceName,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$Command,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[PSCredential]$Credential,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[string]$DnsSuffix = "logon.ds.ge.com",
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[int]$ThrottleLimit = 10,
|
||||||
|
|
||||||
|
[Parameter()]
|
||||||
|
[switch]$LogResults
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Helper Functions
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
function Write-Log {
|
||||||
|
param([string]$Message, [string]$Level = "INFO")
|
||||||
|
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
||||||
|
$color = switch ($Level) {
|
||||||
|
"ERROR" { "Red" }
|
||||||
|
"WARNING" { "Yellow" }
|
||||||
|
"SUCCESS" { "Green" }
|
||||||
|
"TASK" { "Cyan" }
|
||||||
|
default { "White" }
|
||||||
|
}
|
||||||
|
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Task Scriptblocks
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
$TaskScripts = @{
|
||||||
|
|
||||||
|
'RestartSpooler' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
try {
|
||||||
|
Stop-Service -Name Spooler -Force -ErrorAction Stop
|
||||||
|
$queuePath = "$env:SystemRoot\System32\spool\PRINTERS"
|
||||||
|
if (Test-Path $queuePath) { Remove-Item "$queuePath\*" -Force -ErrorAction SilentlyContinue }
|
||||||
|
Start-Service -Name Spooler -ErrorAction Stop
|
||||||
|
$status = (Get-Service -Name Spooler).Status
|
||||||
|
$result.Success = ($status -eq 'Running')
|
||||||
|
$result.Output = "Print Spooler restarted. Status: $status"
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'FlushDNS' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
try {
|
||||||
|
$flushResult = & ipconfig /flushdns 2>&1
|
||||||
|
$result.Output = ($flushResult -join " ").Trim()
|
||||||
|
$result.Success = $true
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'ClearTempFiles' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null; FilesDeleted = 0 }
|
||||||
|
try {
|
||||||
|
$tempPaths = @("$env:TEMP", "$env:SystemRoot\Temp")
|
||||||
|
foreach ($path in $tempPaths) {
|
||||||
|
if (Test-Path $path) {
|
||||||
|
$files = Get-ChildItem -Path $path -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
foreach ($file in $files) {
|
||||||
|
try { Remove-Item $file.FullName -Force -Recurse -ErrorAction SilentlyContinue; $result.FilesDeleted++ } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$result.Success = $true
|
||||||
|
$result.Output = "Deleted $($result.FilesDeleted) temp files"
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'DiskCleanup' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null; SpaceFreedMB = 0 }
|
||||||
|
try {
|
||||||
|
$initialFree = (Get-PSDrive C).Free
|
||||||
|
$cleanupPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\VolumeCaches"
|
||||||
|
$categories = @("Temporary Files", "Temporary Setup Files", "Old ChkDsk Files", "Windows Update Cleanup", "Recycle Bin")
|
||||||
|
foreach ($cat in $categories) {
|
||||||
|
$catPath = Join-Path $cleanupPath $cat
|
||||||
|
if (Test-Path $catPath) { Set-ItemProperty -Path $catPath -Name "StateFlags0100" -Value 2 -ErrorAction SilentlyContinue }
|
||||||
|
}
|
||||||
|
Start-Process -FilePath "cleanmgr.exe" -ArgumentList "/sagerun:100" -Wait -WindowStyle Hidden
|
||||||
|
Start-Sleep -Seconds 2
|
||||||
|
$finalFree = (Get-PSDrive C).Free
|
||||||
|
$result.SpaceFreedMB = [math]::Round(($finalFree - $initialFree) / 1MB, 0)
|
||||||
|
$result.Success = $true
|
||||||
|
$result.Output = "Disk cleanup completed. Space freed: $($result.SpaceFreedMB) MB"
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'OptimizeDisk' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
try {
|
||||||
|
$volumes = Get-Volume | Where-Object { $_.DriveType -eq 'Fixed' -and $_.DriveLetter }
|
||||||
|
$optimized = @()
|
||||||
|
foreach ($vol in $volumes) {
|
||||||
|
$driveLetter = $vol.DriveLetter
|
||||||
|
$physicalDisk = Get-PhysicalDisk | Where-Object { $_.DeviceId -eq (Get-Partition -DriveLetter $driveLetter -ErrorAction SilentlyContinue).DiskNumber }
|
||||||
|
$mediaType = if ($physicalDisk) { $physicalDisk.MediaType } else { "Unknown" }
|
||||||
|
try {
|
||||||
|
if ($mediaType -eq 'SSD') { Optimize-Volume -DriveLetter $driveLetter -ReTrim -ErrorAction Stop; $action = "TRIM" }
|
||||||
|
else { Optimize-Volume -DriveLetter $driveLetter -Defrag -ErrorAction Stop; $action = "Defrag" }
|
||||||
|
$optimized += "${driveLetter}:($action)"
|
||||||
|
} catch { $optimized += "${driveLetter}:(Failed)" }
|
||||||
|
}
|
||||||
|
$result.Success = $true
|
||||||
|
$result.Output = "Optimized: $($optimized -join ', ')"
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'SyncTime' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
try {
|
||||||
|
$source = (& w32tm /query /source 2>&1) -join " "
|
||||||
|
$syncResult = & w32tm /resync /force 2>&1
|
||||||
|
$result.Success = ($syncResult -match "success" -or $LASTEXITCODE -eq 0)
|
||||||
|
$result.Output = "Time synced with $source. Current: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'RestartService' = {
|
||||||
|
param($ServiceName)
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
if (-not $ServiceName) { $result.Error = "ServiceName parameter required"; return $result }
|
||||||
|
try {
|
||||||
|
Restart-Service -Name $ServiceName -Force -ErrorAction Stop
|
||||||
|
$status = (Get-Service -Name $ServiceName).Status
|
||||||
|
$result.Success = ($status -eq 'Running')
|
||||||
|
$result.Output = "Service '$ServiceName' restarted. Status: $status"
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'RunCommand' = {
|
||||||
|
param($Command)
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
if (-not $Command) { $result.Error = "Command parameter required"; return $result }
|
||||||
|
try {
|
||||||
|
$output = Invoke-Expression $Command 2>&1
|
||||||
|
$result.Output = ($output | Out-String).Trim()
|
||||||
|
$result.Success = $true
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'GetDiskSpace' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
try {
|
||||||
|
$drives = Get-PSDrive -PSProvider FileSystem | Where-Object { $_.Used -ne $null }
|
||||||
|
$info = foreach ($drive in $drives) {
|
||||||
|
$freeGB = [math]::Round($drive.Free / 1GB, 1)
|
||||||
|
$usedGB = [math]::Round($drive.Used / 1GB, 1)
|
||||||
|
$totalGB = $freeGB + $usedGB
|
||||||
|
$pctFree = if ($totalGB -gt 0) { [math]::Round(($freeGB / $totalGB) * 100, 0) } else { 0 }
|
||||||
|
"$($drive.Name): $freeGB GB free ($pctFree%)"
|
||||||
|
}
|
||||||
|
$result.Output = $info -join ", "
|
||||||
|
$result.Success = $true
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'GetUptime' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
try {
|
||||||
|
$os = Get-CimInstance -ClassName Win32_OperatingSystem
|
||||||
|
$uptime = (Get-Date) - $os.LastBootUpTime
|
||||||
|
$result.Output = "Up $([math]::Floor($uptime.TotalDays))d $($uptime.Hours)h $($uptime.Minutes)m (Last boot: $($os.LastBootUpTime.ToString('yyyy-MM-dd HH:mm')))"
|
||||||
|
$result.Success = $true
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'TestConnection' = {
|
||||||
|
$result = @{ Success = $true; Hostname = $env:COMPUTERNAME; Output = "Connection successful"; Error = $null }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
|
||||||
|
'RestartComputer' = {
|
||||||
|
$result = @{ Success = $false; Hostname = $env:COMPUTERNAME; Output = ""; Error = $null }
|
||||||
|
try {
|
||||||
|
$result.Output = "Restart initiated"
|
||||||
|
$result.Success = $true
|
||||||
|
# Schedule restart in 5 seconds to allow response to return
|
||||||
|
Start-Process -FilePath "shutdown.exe" -ArgumentList "/r /t 5 /c `"Remote restart initiated via WinRM`"" -NoNewWindow
|
||||||
|
} catch { $result.Error = $_.Exception.Message }
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# Main Execution
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ("=" * 60) -ForegroundColor Cyan
|
||||||
|
Write-Host " Remote Task Executor - Task: $Task" -ForegroundColor Cyan
|
||||||
|
Write-Host ("=" * 60) -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Validate task-specific parameters
|
||||||
|
if ($Task -eq 'RestartService' -and -not $ServiceName) {
|
||||||
|
Write-Log "ServiceName parameter is required for RestartService task" -Level "ERROR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Task -eq 'RunCommand' -and -not $Command) {
|
||||||
|
Write-Log "Command parameter is required for RunCommand task" -Level "ERROR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if ($Task -eq 'RestartComputer') {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "WARNING: This will restart the target computer(s)!" -ForegroundColor Yellow
|
||||||
|
$confirm = Read-Host "Type 'YES' to confirm"
|
||||||
|
if ($confirm -ne 'YES') {
|
||||||
|
Write-Log "Restart cancelled by user" -Level "WARNING"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get credentials
|
||||||
|
if (-not $Credential) {
|
||||||
|
Write-Log "Enter credentials for remote PCs:" -Level "INFO"
|
||||||
|
$Credential = Get-Credential -Message "Enter admin credentials for remote PCs"
|
||||||
|
if (-not $Credential) {
|
||||||
|
Write-Log "Credentials required. Exiting." -Level "ERROR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build computer list
|
||||||
|
$computers = @()
|
||||||
|
|
||||||
|
if ($ComputerName) {
|
||||||
|
$computers = $ComputerName
|
||||||
|
} else {
|
||||||
|
if (-not (Test-Path $HostsFile)) {
|
||||||
|
Write-Log "Hosts file not found: $HostsFile" -Level "ERROR"
|
||||||
|
Write-Log "Create a text file with one hostname or IP per line." -Level "INFO"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$computers = Get-Content $HostsFile | Where-Object { $_.Trim() -and -not $_.StartsWith("#") }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($computers.Count -eq 0) {
|
||||||
|
Write-Log "No computers specified." -Level "ERROR"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Log "Target computers: $($computers.Count)" -Level "INFO"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Build FQDNs if DNS suffix provided
|
||||||
|
$targets = $computers | ForEach-Object {
|
||||||
|
$name = $_.Trim()
|
||||||
|
if ($DnsSuffix -and $name -notlike "*.*") {
|
||||||
|
"$name.$DnsSuffix"
|
||||||
|
} else {
|
||||||
|
$name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get the scriptblock
|
||||||
|
$scriptBlock = $TaskScripts[$Task]
|
||||||
|
|
||||||
|
# Create session options
|
||||||
|
$sessionOption = New-PSSessionOption -OpenTimeout 30000 -OperationTimeout 300000 -NoMachineProfile
|
||||||
|
|
||||||
|
Write-Log "Executing on $($targets.Count) computer(s) in parallel (ThrottleLimit: $ThrottleLimit)..." -Level "INFO"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Build arguments for tasks that need them
|
||||||
|
$taskArgs = @()
|
||||||
|
if ($Task -eq 'RestartService') { $taskArgs = @($ServiceName) }
|
||||||
|
if ($Task -eq 'RunCommand') { $taskArgs = @($Command) }
|
||||||
|
|
||||||
|
# Show progress indicator
|
||||||
|
$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||||
|
Write-Host " [" -NoNewline
|
||||||
|
Write-Host "Running..." -ForegroundColor Yellow -NoNewline
|
||||||
|
Write-Host "] Please wait..." -NoNewline
|
||||||
|
|
||||||
|
# Execute on all remote computers in parallel using Invoke-Command
|
||||||
|
$results = @()
|
||||||
|
$connectionErrors = @()
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($taskArgs.Count -gt 0) {
|
||||||
|
$results = Invoke-Command -ComputerName $targets -ScriptBlock $scriptBlock -ArgumentList $taskArgs `
|
||||||
|
-Credential $Credential -SessionOption $sessionOption -Authentication Negotiate `
|
||||||
|
-ThrottleLimit $ThrottleLimit -ErrorAction SilentlyContinue -ErrorVariable connectionErrors
|
||||||
|
} else {
|
||||||
|
$results = Invoke-Command -ComputerName $targets -ScriptBlock $scriptBlock `
|
||||||
|
-Credential $Credential -SessionOption $sessionOption -Authentication Negotiate `
|
||||||
|
-ThrottleLimit $ThrottleLimit -ErrorAction SilentlyContinue -ErrorVariable connectionErrors
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Log "Execution error: $($_.Exception.Message)" -Level "ERROR"
|
||||||
|
}
|
||||||
|
|
||||||
|
$stopwatch.Stop()
|
||||||
|
$elapsed = $stopwatch.Elapsed.TotalSeconds
|
||||||
|
|
||||||
|
# Clear the progress line
|
||||||
|
Write-Host "`r" -NoNewline
|
||||||
|
Write-Host (" " * 60) -NoNewline
|
||||||
|
Write-Host "`r" -NoNewline
|
||||||
|
Write-Log "Completed in $([math]::Round($elapsed, 1)) seconds" -Level "INFO"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Collect all results (successes, failures, connection errors)
|
||||||
|
$allResults = @()
|
||||||
|
$successCount = 0
|
||||||
|
$failCount = 0
|
||||||
|
|
||||||
|
foreach ($result in $results) {
|
||||||
|
$status = if ($result.Success) { "OK"; $successCount++ } else { "FAIL"; $failCount++ }
|
||||||
|
$message = if ($result.Success) { $result.Output } else { $result.Error }
|
||||||
|
$allResults += [PSCustomObject]@{
|
||||||
|
Status = $status
|
||||||
|
Computer = $result.Hostname
|
||||||
|
Message = $message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process connection errors
|
||||||
|
foreach ($err in $connectionErrors) {
|
||||||
|
$targetName = if ($err.TargetObject) { $err.TargetObject } else { "Unknown" }
|
||||||
|
# Extract just the computer name from FQDN for display
|
||||||
|
$shortName = ($targetName -split '\.')[0]
|
||||||
|
$errorMsg = $err.Exception.Message -replace '\r?\n', ' '
|
||||||
|
# Truncate long error messages
|
||||||
|
if ($errorMsg.Length -gt 60) { $errorMsg = $errorMsg.Substring(0, 57) + "..." }
|
||||||
|
$allResults += [PSCustomObject]@{
|
||||||
|
Status = "FAIL"
|
||||||
|
Computer = $shortName
|
||||||
|
Message = $errorMsg
|
||||||
|
}
|
||||||
|
$failCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
# Sort results: failures first, then successes
|
||||||
|
$allResults = $allResults | Sort-Object @{Expression={$_.Status}; Descending=$true}, Computer
|
||||||
|
|
||||||
|
# Display results in a formatted table
|
||||||
|
Write-Host " STATUS COMPUTER RESULT" -ForegroundColor Cyan
|
||||||
|
Write-Host " ------ -------- ------" -ForegroundColor Cyan
|
||||||
|
|
||||||
|
foreach ($r in $allResults) {
|
||||||
|
$statusColor = if ($r.Status -eq "OK") { "Green" } else { "Red" }
|
||||||
|
$statusIcon = if ($r.Status -eq "OK") { "[OK] " } else { "[FAIL]" }
|
||||||
|
|
||||||
|
# Pad/truncate computer name to 20 chars
|
||||||
|
$compName = $r.Computer
|
||||||
|
if ($compName.Length -gt 18) { $compName = $compName.Substring(0, 15) + "..." }
|
||||||
|
$compName = $compName.PadRight(20)
|
||||||
|
|
||||||
|
# Truncate message if too long
|
||||||
|
$msg = $r.Message
|
||||||
|
if ($msg.Length -gt 50) { $msg = $msg.Substring(0, 47) + "..." }
|
||||||
|
|
||||||
|
Write-Host " " -NoNewline
|
||||||
|
Write-Host $statusIcon -ForegroundColor $statusColor -NoNewline
|
||||||
|
Write-Host " $compName " -NoNewline
|
||||||
|
Write-Host $msg -ForegroundColor $(if ($r.Status -eq "OK") { "White" } else { "Yellow" })
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host ("=" * 60) -ForegroundColor Cyan
|
||||||
|
Write-Host " SUMMARY" -ForegroundColor Cyan
|
||||||
|
Write-Host ("=" * 60) -ForegroundColor Cyan
|
||||||
|
Write-Host " Task: $Task" -ForegroundColor White
|
||||||
|
Write-Host " Total: $($computers.Count)" -ForegroundColor White
|
||||||
|
Write-Host " Successful: $successCount" -ForegroundColor Green
|
||||||
|
Write-Host " Failed: $failCount" -ForegroundColor $(if ($failCount -gt 0) { "Red" } else { "White" })
|
||||||
|
Write-Host ("=" * 60) -ForegroundColor Cyan
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Save to log file if requested
|
||||||
|
if ($LogResults) {
|
||||||
|
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||||
|
if (-not $scriptDir) { $scriptDir = Get-Location }
|
||||||
|
$logTimestamp = Get-Date -Format "yyyyMMdd_HHmmss"
|
||||||
|
$logFile = Join-Path $scriptDir "RemoteTask_$logTimestamp.log"
|
||||||
|
|
||||||
|
$logContent = @()
|
||||||
|
$logContent += "=" * 60
|
||||||
|
$logContent += "Remote Task Execution Log"
|
||||||
|
$logContent += "=" * 60
|
||||||
|
$logContent += "Date/Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
|
||||||
|
$logContent += "Task: $Task"
|
||||||
|
$logContent += "Targets: $($computers.Count)"
|
||||||
|
$logContent += "ThrottleLimit: $ThrottleLimit"
|
||||||
|
$logContent += "Elapsed: $([math]::Round($elapsed, 1)) seconds"
|
||||||
|
if ($ServiceName) { $logContent += "ServiceName: $ServiceName" }
|
||||||
|
if ($Command) { $logContent += "Command: $Command" }
|
||||||
|
$logContent += ""
|
||||||
|
$logContent += "=" * 60
|
||||||
|
$logContent += "RESULTS"
|
||||||
|
$logContent += "=" * 60
|
||||||
|
$logContent += ""
|
||||||
|
$logContent += "STATUS COMPUTER RESULT"
|
||||||
|
$logContent += "------ -------- ------"
|
||||||
|
|
||||||
|
foreach ($r in $allResults) {
|
||||||
|
$statusText = if ($r.Status -eq "OK") { "[OK] " } else { "[FAIL] " }
|
||||||
|
$compText = $r.Computer.PadRight(28)
|
||||||
|
$logContent += "$statusText $compText $($r.Message)"
|
||||||
|
}
|
||||||
|
|
||||||
|
$logContent += ""
|
||||||
|
$logContent += "=" * 60
|
||||||
|
$logContent += "SUMMARY"
|
||||||
|
$logContent += "=" * 60
|
||||||
|
$logContent += "Total: $($computers.Count)"
|
||||||
|
$logContent += "Successful: $successCount"
|
||||||
|
$logContent += "Failed: $failCount"
|
||||||
|
$logContent += "=" * 60
|
||||||
|
|
||||||
|
$logContent | Out-File -FilePath $logFile -Encoding UTF8
|
||||||
|
Write-Log "Results saved to: $logFile" -Level "SUCCESS"
|
||||||
|
Write-Host ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Results are displayed above and optionally saved to log file
|
||||||
|
# To capture results programmatically, use: $results = Invoke-Command ... directly
|
||||||
296
winrm-setup-package/README.md
Normal file
296
winrm-setup-package/README.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
# WinRM Setup Package for Shopfloor PCs
|
||||||
|
|
||||||
|
This package provides scripts to configure WinRM (Windows Remote Management) on shopfloor PCs and execute remote maintenance tasks.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
| File | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `Setup-WinRM.bat` | Run on each PC to enable and configure WinRM |
|
||||||
|
| `Invoke-RemoteTask.ps1` | PowerShell script to execute tasks on remote PCs |
|
||||||
|
| `hosts.txt` | List of target computers (edit before use) |
|
||||||
|
| `README.md` | This documentation |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Step 1: Configure Your Admin Workstation
|
||||||
|
|
||||||
|
Before connecting to remote PCs, run this once on your admin workstation (as Administrator):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows your workstation to connect to any PC in the domain.
|
||||||
|
|
||||||
|
### Step 2: Configure the Setup Script
|
||||||
|
|
||||||
|
Edit `Setup-WinRM.bat` and update these values at the top:
|
||||||
|
|
||||||
|
```batch
|
||||||
|
REM Default security group - who can use WinRM to connect
|
||||||
|
set "DEFAULT_SECURITY_GROUP=logon\groupid"
|
||||||
|
|
||||||
|
REM Where to log the inventory CSV (network share recommended)
|
||||||
|
set "DEFAULT_LOG_PATH=\\server\share\winrm-inventory"
|
||||||
|
|
||||||
|
REM Domain suffix for TrustedHosts
|
||||||
|
set "TRUSTED_DOMAIN=*.logon.ds.ge.com"
|
||||||
|
|
||||||
|
REM Optional: Trust a specific subnet (uncomment and set)
|
||||||
|
REM set "TRUSTED_SUBNET=10.48.130.*"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 3: Create Security Group in Active Directory
|
||||||
|
|
||||||
|
1. Open **Active Directory Users and Computers**
|
||||||
|
2. Create a new Security Group (or use existing group matching `groupid`)
|
||||||
|
3. Add users who should have remote management access
|
||||||
|
|
||||||
|
### Step 4: Run Setup on Each Shopfloor PC
|
||||||
|
|
||||||
|
Run as Administrator on each PC:
|
||||||
|
|
||||||
|
```cmd
|
||||||
|
Setup-WinRM.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with parameters:
|
||||||
|
```cmd
|
||||||
|
Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory"
|
||||||
|
```
|
||||||
|
|
||||||
|
The script will:
|
||||||
|
- Enable WinRM service
|
||||||
|
- Configure authentication (Negotiate/Kerberos)
|
||||||
|
- Set firewall rules (domain profile only)
|
||||||
|
- Restrict access to the security group
|
||||||
|
- Log hostname/IP to CSV inventory
|
||||||
|
|
||||||
|
### Step 5: Run Remote Tasks
|
||||||
|
|
||||||
|
From your admin workstation, edit `hosts.txt` with target PCs, then:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Test connectivity
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task TestConnection
|
||||||
|
|
||||||
|
# Restart print spooler on all hosts
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
|
||||||
|
|
||||||
|
# Check disk space
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace
|
||||||
|
|
||||||
|
# Run on a single PC
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task FlushDNS
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Setup Script Details
|
||||||
|
|
||||||
|
### What Setup-WinRM.bat Configures
|
||||||
|
|
||||||
|
| Setting | Value | Purpose |
|
||||||
|
|---------|-------|---------|
|
||||||
|
| WinRM Service | Auto-start | Ensures WinRM starts on boot |
|
||||||
|
| AllowUnencrypted | false | Security: require encrypted connections |
|
||||||
|
| Negotiate Auth | true | Enables Kerberos/NTLM authentication |
|
||||||
|
| CredSSP Auth | true | Enables credential delegation (double-hop) |
|
||||||
|
| Firewall | Domain profile | Opens port 5985 for domain connections only |
|
||||||
|
| TrustedHosts | *.logon.ds.ge.com | Trusts domain-joined PCs |
|
||||||
|
| RootSDDL | Security group | Restricts who can connect |
|
||||||
|
|
||||||
|
### CSV Inventory
|
||||||
|
|
||||||
|
The setup script logs each PC to a CSV file:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup
|
||||||
|
PC001,10.48.130.101,2026-01-08 09:30:00,10.0,logon\groupid
|
||||||
|
PC002,10.48.130.102,2026-01-08 09:35:00,10.0,logon\groupid
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use this CSV as your hosts file:
|
||||||
|
```powershell
|
||||||
|
# Extract hostnames from CSV
|
||||||
|
Import-Csv "\\server\share\winrm-inventory\winrm-inventory.csv" |
|
||||||
|
Select-Object -ExpandProperty Hostname |
|
||||||
|
Set-Content .\hosts.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Available Remote Tasks
|
||||||
|
|
||||||
|
| Task | Description |
|
||||||
|
|------|-------------|
|
||||||
|
| `TestConnection` | Verify WinRM connectivity |
|
||||||
|
| `GetUptime` | Show system uptime and last boot time |
|
||||||
|
| `GetDiskSpace` | Show free space on all drives |
|
||||||
|
| `RestartSpooler` | Restart Print Spooler service |
|
||||||
|
| `FlushDNS` | Clear DNS resolver cache |
|
||||||
|
| `ClearTempFiles` | Delete Windows temp files |
|
||||||
|
| `DiskCleanup` | Run Windows Disk Cleanup |
|
||||||
|
| `OptimizeDisk` | TRIM (SSD) or Defrag (HDD) |
|
||||||
|
| `SyncTime` | Force time sync with domain controller |
|
||||||
|
| `RestartService` | Restart any Windows service (requires `-ServiceName`) |
|
||||||
|
| `RunCommand` | Run custom PowerShell command (requires `-Command`) |
|
||||||
|
| `RestartComputer` | Restart the remote PC (requires YES confirmation) |
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Check uptime on all hosts
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task GetUptime
|
||||||
|
|
||||||
|
# Restart a specific service
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName "Spooler"
|
||||||
|
|
||||||
|
# Run custom command
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RunCommand -Command "Get-Process | Sort CPU -Desc | Select -First 5"
|
||||||
|
|
||||||
|
# Use custom hosts file
|
||||||
|
.\Invoke-RemoteTask.ps1 -HostsFile ".\cnc-machines.txt" -Task FlushDNS
|
||||||
|
|
||||||
|
# Specify DNS suffix for short hostnames
|
||||||
|
.\Invoke-RemoteTask.ps1 -DnsSuffix "logon.ds.ge.com" -Task TestConnection
|
||||||
|
|
||||||
|
# Restart a remote PC (will prompt for confirmation)
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName "PC001" -Task RestartComputer
|
||||||
|
|
||||||
|
# Increase parallelism for faster execution on many PCs
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task FlushDNS -ThrottleLimit 20
|
||||||
|
|
||||||
|
# Save results to a log file
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace -LogResults
|
||||||
|
```
|
||||||
|
|
||||||
|
### Logging Results
|
||||||
|
|
||||||
|
Use `-LogResults` to save task output to a timestamped log file in the script directory:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartSpooler -LogResults
|
||||||
|
# Creates: RemoteTask_20260108_143022.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Log files contain:
|
||||||
|
- Task name and parameters
|
||||||
|
- Execution time
|
||||||
|
- Status of each computer (OK/FAIL)
|
||||||
|
- Result messages
|
||||||
|
- Summary totals
|
||||||
|
|
||||||
|
### Targeting Multiple PCs
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# Comma-separated list
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName "PC001","PC002","PC003" -Task GetUptime
|
||||||
|
|
||||||
|
# Array variable
|
||||||
|
$pcs = @("PC001", "PC002", "PC003")
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task FlushDNS
|
||||||
|
|
||||||
|
# From hosts.txt file (default)
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
|
||||||
|
|
||||||
|
# From CSV inventory
|
||||||
|
$pcs = (Import-Csv "\\server\share\winrm-inventory.csv").Hostname
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task GetDiskSpace
|
||||||
|
|
||||||
|
# From Active Directory query
|
||||||
|
$pcs = (Get-ADComputer -Filter "Name -like 'SHOPFLOOR-*'").Name
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task SyncTime
|
||||||
|
```
|
||||||
|
|
||||||
|
All commands run in parallel (default: 10 concurrent connections, adjust with `-ThrottleLimit`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### "Access Denied" when connecting
|
||||||
|
|
||||||
|
1. Verify you're a member of the WinRM security group
|
||||||
|
2. Check that your credentials are correct
|
||||||
|
3. Verify the target PC ran Setup-WinRM.bat successfully
|
||||||
|
|
||||||
|
### "WinRM cannot complete the operation"
|
||||||
|
|
||||||
|
1. Verify the target PC is reachable: `ping PC001`
|
||||||
|
2. Check WinRM is running on target: `sc query winrm` (on target PC)
|
||||||
|
3. Verify firewall allows port 5985
|
||||||
|
|
||||||
|
### "The WinRM client cannot process the request"
|
||||||
|
|
||||||
|
1. Add target to TrustedHosts on your admin workstation:
|
||||||
|
```powershell
|
||||||
|
Set-Item WSMan:\localhost\Client\TrustedHosts -Value "*.logon.ds.ge.com" -Force
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test WinRM Configuration
|
||||||
|
|
||||||
|
On your admin workstation:
|
||||||
|
```powershell
|
||||||
|
# Test basic connectivity
|
||||||
|
Test-WSMan -ComputerName PC001
|
||||||
|
|
||||||
|
# Test with credentials
|
||||||
|
$cred = Get-Credential
|
||||||
|
Test-WSMan -ComputerName PC001 -Credential $cred -Authentication Negotiate
|
||||||
|
|
||||||
|
# Enter interactive session
|
||||||
|
Enter-PSSession -ComputerName PC001 -Credential $cred -Authentication Negotiate
|
||||||
|
```
|
||||||
|
|
||||||
|
On the target PC:
|
||||||
|
```cmd
|
||||||
|
winrm enumerate winrm/config/listener
|
||||||
|
winrm get winrm/config/service
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Use Security Groups**: Always restrict WinRM access to a specific AD group
|
||||||
|
2. **Domain Profile Only**: Firewall rules only allow connections on domain networks
|
||||||
|
3. **No Unencrypted Traffic**: AllowUnencrypted is set to false
|
||||||
|
4. **Audit Access**: Enable Windows Security auditing for logon events
|
||||||
|
5. **Credential Protection**: Use dedicated admin accounts, not personal accounts
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding Custom Tasks
|
||||||
|
|
||||||
|
Edit `Invoke-RemoteTask.ps1` and add to the `$TaskScripts` hashtable:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
'MyCustomTask' = {
|
||||||
|
$result = @{
|
||||||
|
Success = $false
|
||||||
|
Hostname = $env:COMPUTERNAME
|
||||||
|
Output = ""
|
||||||
|
Error = $null
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
# Your code here
|
||||||
|
$result.Output = "Task completed"
|
||||||
|
$result.Success = $true
|
||||||
|
} catch {
|
||||||
|
$result.Error = $_.Exception.Message
|
||||||
|
}
|
||||||
|
return $result
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add the task name to the `ValidateSet` in the param block.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions, contact your IT support team.
|
||||||
269
winrm-setup-package/Setup-WinRM.bat
Normal file
269
winrm-setup-package/Setup-WinRM.bat
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
@echo off
|
||||||
|
REM ============================================================================
|
||||||
|
REM WinRM Setup Script for Shopfloor PCs
|
||||||
|
REM ============================================================================
|
||||||
|
REM
|
||||||
|
REM PURPOSE: Configures WinRM on a Windows PC and restricts access to members
|
||||||
|
REM of a specific Active Directory security group. Logs setup to CSV.
|
||||||
|
REM
|
||||||
|
REM USAGE: Run as Administrator on each shopfloor PC
|
||||||
|
REM Setup-WinRM.bat [SecurityGroupName] [LogPath]
|
||||||
|
REM
|
||||||
|
REM EXAMPLE: Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory"
|
||||||
|
REM
|
||||||
|
REM REQUIREMENTS:
|
||||||
|
REM - Must be run as Administrator
|
||||||
|
REM - PC must be domain-joined
|
||||||
|
REM - Security group must exist in Active Directory
|
||||||
|
REM
|
||||||
|
REM ============================================================================
|
||||||
|
|
||||||
|
setlocal EnableDelayedExpansion
|
||||||
|
|
||||||
|
REM Check for admin privileges
|
||||||
|
net session >nul 2>&1
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo.
|
||||||
|
echo ERROR: This script must be run as Administrator.
|
||||||
|
echo Right-click and select "Run as administrator"
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM ============================================================================
|
||||||
|
REM Configuration - EDIT THESE VALUES FOR YOUR ENVIRONMENT
|
||||||
|
REM ============================================================================
|
||||||
|
|
||||||
|
REM Default security group (can be overridden by parameter)
|
||||||
|
set "DEFAULT_SECURITY_GROUP=logon\groupid"
|
||||||
|
|
||||||
|
REM Default log path for CSV inventory (can be overridden by parameter)
|
||||||
|
REM Use a network share that all PCs can write to
|
||||||
|
set "DEFAULT_LOG_PATH=\\server\share\winrm-inventory"
|
||||||
|
|
||||||
|
REM Domain suffix for TrustedHosts (e.g., *.logon.ds.ge.com)
|
||||||
|
set "TRUSTED_DOMAIN=*.logon.ds.ge.com"
|
||||||
|
|
||||||
|
REM Optional: Trusted subnets - comma-separated (leave empty to skip)
|
||||||
|
REM For /24 subnet: "10.48.130.*"
|
||||||
|
REM For /23 subnet: "10.48.130.*,10.48.131.*"
|
||||||
|
REM For /22 subnet: "10.48.128.*,10.48.129.*,10.48.130.*,10.48.131.*"
|
||||||
|
REM set "TRUSTED_SUBNET=10.48.130.*,10.48.131.*"
|
||||||
|
set "TRUSTED_SUBNET="
|
||||||
|
|
||||||
|
REM ============================================================================
|
||||||
|
|
||||||
|
REM Get parameters or use defaults
|
||||||
|
set "SECURITY_GROUP=%~1"
|
||||||
|
set "LOG_PATH=%~2"
|
||||||
|
|
||||||
|
if "%SECURITY_GROUP%"=="" set "SECURITY_GROUP=%DEFAULT_SECURITY_GROUP%"
|
||||||
|
if "%LOG_PATH%"=="" set "LOG_PATH=%DEFAULT_LOG_PATH%"
|
||||||
|
|
||||||
|
echo.
|
||||||
|
echo ============================================================================
|
||||||
|
echo WinRM Setup Script
|
||||||
|
echo ============================================================================
|
||||||
|
echo.
|
||||||
|
echo Computer: %COMPUTERNAME%
|
||||||
|
echo Security Group: %SECURITY_GROUP%
|
||||||
|
echo Log Path: %LOG_PATH%
|
||||||
|
echo Trusted Domain: %TRUSTED_DOMAIN%
|
||||||
|
if not "%TRUSTED_SUBNET%"=="" echo Trusted Subnet: %TRUSTED_SUBNET%
|
||||||
|
echo.
|
||||||
|
echo ============================================================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Step 1: Enable WinRM service
|
||||||
|
echo [1/7] Enabling WinRM service...
|
||||||
|
sc config WinRM start= auto >nul 2>&1
|
||||||
|
net start WinRM >nul 2>&1
|
||||||
|
if %ERRORLEVEL% equ 0 (
|
||||||
|
echo WinRM service started
|
||||||
|
) else (
|
||||||
|
echo WinRM service already running
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Step 2: Run quick config (creates listener, firewall rules)
|
||||||
|
echo [2/7] Running WinRM quick configuration...
|
||||||
|
winrm quickconfig -quiet >nul 2>&1
|
||||||
|
echo Quick config completed
|
||||||
|
|
||||||
|
REM Step 3: Configure WinRM settings
|
||||||
|
echo [3/7] Configuring WinRM settings...
|
||||||
|
|
||||||
|
REM Disable unencrypted traffic (security best practice)
|
||||||
|
winrm set winrm/config/service @{AllowUnencrypted="false"} >nul 2>&1
|
||||||
|
|
||||||
|
REM Enable Negotiate authentication (Kerberos/NTLM)
|
||||||
|
winrm set winrm/config/service/auth @{Negotiate="true"} >nul 2>&1
|
||||||
|
|
||||||
|
REM Enable CredSSP for double-hop scenarios (optional)
|
||||||
|
winrm set winrm/config/service/auth @{CredSSP="true"} >nul 2>&1
|
||||||
|
|
||||||
|
REM Set max concurrent operations
|
||||||
|
winrm set winrm/config/service @{MaxConcurrentOperationsPerUser="50"} >nul 2>&1
|
||||||
|
|
||||||
|
REM Set max memory per shell (512MB)
|
||||||
|
winrm set winrm/config/winrs @{MaxMemoryPerShellMB="512"} >nul 2>&1
|
||||||
|
|
||||||
|
echo WinRM settings configured
|
||||||
|
|
||||||
|
REM Step 4: Configure TrustedHosts on CLIENT side (for the admin workstation)
|
||||||
|
REM This step configures this PC to trust connections TO other PCs
|
||||||
|
echo [4/7] Configuring TrustedHosts...
|
||||||
|
|
||||||
|
REM Build TrustedHosts value
|
||||||
|
set "TRUSTED_HOSTS=%TRUSTED_DOMAIN%"
|
||||||
|
if not "%TRUSTED_SUBNET%"=="" (
|
||||||
|
set "TRUSTED_HOSTS=%TRUSTED_HOSTS%,%TRUSTED_SUBNET%"
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Get current TrustedHosts and append if needed
|
||||||
|
powershell -ExecutionPolicy Bypass -Command ^
|
||||||
|
"$currentTrusted = (Get-Item WSMan:\localhost\Client\TrustedHosts -ErrorAction SilentlyContinue).Value; " ^
|
||||||
|
"$newHosts = '%TRUSTED_HOSTS%'; " ^
|
||||||
|
"if ([string]::IsNullOrEmpty($currentTrusted)) { " ^
|
||||||
|
" Set-Item WSMan:\localhost\Client\TrustedHosts -Value $newHosts -Force; " ^
|
||||||
|
" Write-Host ' Set TrustedHosts: ' $newHosts; " ^
|
||||||
|
"} elseif ($currentTrusted -notlike '*%TRUSTED_DOMAIN%*') { " ^
|
||||||
|
" $combined = $currentTrusted + ',' + $newHosts; " ^
|
||||||
|
" Set-Item WSMan:\localhost\Client\TrustedHosts -Value $combined -Force; " ^
|
||||||
|
" Write-Host ' Added to TrustedHosts: ' $newHosts; " ^
|
||||||
|
"} else { " ^
|
||||||
|
" Write-Host ' TrustedHosts already configured'; " ^
|
||||||
|
"}"
|
||||||
|
|
||||||
|
REM Step 5: Configure firewall rules
|
||||||
|
echo [5/7] Configuring firewall rules...
|
||||||
|
|
||||||
|
REM Enable WinRM firewall rule for domain profile
|
||||||
|
netsh advfirewall firewall set rule name="Windows Remote Management (HTTP-In)" new enable=yes profile=domain >nul 2>&1
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
netsh advfirewall firewall add rule name="Windows Remote Management (HTTP-In)" dir=in action=allow protocol=tcp localport=5985 profile=domain >nul 2>&1
|
||||||
|
)
|
||||||
|
echo Firewall rule enabled for domain profile
|
||||||
|
|
||||||
|
REM Step 6: Set WinRM permissions for security group
|
||||||
|
echo [6/7] Configuring WinRM permissions for security group...
|
||||||
|
|
||||||
|
powershell -ExecutionPolicy Bypass -Command ^
|
||||||
|
"$group = '%SECURITY_GROUP%'; " ^
|
||||||
|
"try { " ^
|
||||||
|
" $ntAccount = New-Object System.Security.Principal.NTAccount($group); " ^
|
||||||
|
" $sid = $ntAccount.Translate([System.Security.Principal.SecurityIdentifier]); " ^
|
||||||
|
" $sidString = $sid.Value; " ^
|
||||||
|
" Write-Host ' Group SID: ' $sidString; " ^
|
||||||
|
" $currentSDDL = (Get-Item WSMan:\localhost\Service\RootSDDL).Value; " ^
|
||||||
|
" $newACE = '(A;;GXGR;;;' + $sidString + ')'; " ^
|
||||||
|
" if ($currentSDDL -notmatch [regex]::Escape($sidString)) { " ^
|
||||||
|
" $newSDDL = $currentSDDL -replace 'D:', ('D:' + $newACE); " ^
|
||||||
|
" Set-Item WSMan:\localhost\Service\RootSDDL -Value $newSDDL -Force; " ^
|
||||||
|
" Write-Host ' Added security group to WinRM permissions'; " ^
|
||||||
|
" } else { " ^
|
||||||
|
" Write-Host ' Security group already has WinRM permissions'; " ^
|
||||||
|
" } " ^
|
||||||
|
"} catch { " ^
|
||||||
|
" Write-Host ' ERROR: Could not resolve security group - ' $_.Exception.Message; " ^
|
||||||
|
" exit 1; " ^
|
||||||
|
"}"
|
||||||
|
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo.
|
||||||
|
echo ERROR: Failed to configure security group permissions.
|
||||||
|
echo Verify the security group exists in Active Directory.
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Step 7: Log to CSV inventory file
|
||||||
|
echo [7/7] Logging to inventory CSV...
|
||||||
|
|
||||||
|
REM Get IP address
|
||||||
|
for /f "tokens=2 delims=:" %%a in ('ipconfig ^| findstr /i "IPv4"') do (
|
||||||
|
set "IP_ADDRESS=%%a"
|
||||||
|
goto :gotip
|
||||||
|
)
|
||||||
|
:gotip
|
||||||
|
set "IP_ADDRESS=%IP_ADDRESS: =%"
|
||||||
|
|
||||||
|
REM Get current date/time
|
||||||
|
for /f "tokens=2 delims==" %%a in ('wmic os get localdatetime /value') do set "DT=%%a"
|
||||||
|
set "SETUP_DATE=%DT:~0,4%-%DT:~4,2%-%DT:~6,2% %DT:~8,2%:%DT:~10,2%:%DT:~12,2%"
|
||||||
|
|
||||||
|
REM Get OS version
|
||||||
|
for /f "tokens=4-5 delims=. " %%a in ('ver') do set "OS_VERSION=%%a.%%b"
|
||||||
|
|
||||||
|
REM Create CSV directory if it doesn't exist
|
||||||
|
if not exist "%LOG_PATH%" (
|
||||||
|
mkdir "%LOG_PATH%" 2>nul
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo WARNING: Could not create log directory. Logging skipped.
|
||||||
|
goto :skiplog
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Define CSV file
|
||||||
|
set "CSV_FILE=%LOG_PATH%\winrm-inventory.csv"
|
||||||
|
|
||||||
|
REM Create CSV header if file doesn't exist
|
||||||
|
if not exist "%CSV_FILE%" (
|
||||||
|
echo Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup > "%CSV_FILE%"
|
||||||
|
if %ERRORLEVEL% neq 0 (
|
||||||
|
echo WARNING: Could not create CSV file. Logging skipped.
|
||||||
|
goto :skiplog
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if this hostname already exists in CSV and update or append
|
||||||
|
powershell -ExecutionPolicy Bypass -Command ^
|
||||||
|
"$csvFile = '%CSV_FILE%'; " ^
|
||||||
|
"$hostname = '%COMPUTERNAME%'; " ^
|
||||||
|
"$newLine = '%COMPUTERNAME%,%IP_ADDRESS%,%SETUP_DATE%,%OS_VERSION%,%SECURITY_GROUP%'; " ^
|
||||||
|
"try { " ^
|
||||||
|
" $content = Get-Content $csvFile -ErrorAction SilentlyContinue; " ^
|
||||||
|
" $found = $false; " ^
|
||||||
|
" $newContent = @(); " ^
|
||||||
|
" foreach ($line in $content) { " ^
|
||||||
|
" if ($line -like \"$hostname,*\") { " ^
|
||||||
|
" $newContent += $newLine; " ^
|
||||||
|
" $found = $true; " ^
|
||||||
|
" } else { " ^
|
||||||
|
" $newContent += $line; " ^
|
||||||
|
" } " ^
|
||||||
|
" } " ^
|
||||||
|
" if (-not $found) { $newContent += $newLine; } " ^
|
||||||
|
" $newContent | Set-Content $csvFile -Force; " ^
|
||||||
|
" Write-Host ' Logged: %COMPUTERNAME% (%IP_ADDRESS%)'; " ^
|
||||||
|
"} catch { " ^
|
||||||
|
" Write-Host ' WARNING: Could not write to CSV - ' $_.Exception.Message; " ^
|
||||||
|
"}"
|
||||||
|
|
||||||
|
:skiplog
|
||||||
|
|
||||||
|
REM Verify configuration
|
||||||
|
echo.
|
||||||
|
echo ============================================================================
|
||||||
|
echo WinRM Setup Complete!
|
||||||
|
echo ============================================================================
|
||||||
|
echo.
|
||||||
|
echo Computer: %COMPUTERNAME%
|
||||||
|
echo IP Address: %IP_ADDRESS%
|
||||||
|
echo Security Group: %SECURITY_GROUP%
|
||||||
|
echo WinRM Port: 5985 (HTTP)
|
||||||
|
echo Trusted Hosts: %TRUSTED_HOSTS%
|
||||||
|
echo.
|
||||||
|
echo Inventory logged to: %CSV_FILE%
|
||||||
|
echo.
|
||||||
|
echo Members of '%SECURITY_GROUP%' can now connect using:
|
||||||
|
echo Enter-PSSession -ComputerName %COMPUTERNAME% -Credential (Get-Credential)
|
||||||
|
echo.
|
||||||
|
echo To test from a remote PC (as a member of the security group):
|
||||||
|
echo Test-WSMan -ComputerName %COMPUTERNAME%
|
||||||
|
echo.
|
||||||
|
echo ============================================================================
|
||||||
|
|
||||||
|
pause
|
||||||
|
exit /b 0
|
||||||
424
winrm-setup-package/WinRM-Setup-Guide.html
Normal file
424
winrm-setup-package/WinRM-Setup-Guide.html
Normal file
@@ -0,0 +1,424 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WinRM Setup Package for Shopfloor PCs</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
max-width: 900px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #0078d4;
|
||||||
|
border-bottom: 3px solid #0078d4;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
color: #106ebe;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
margin-top: 30px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
color: #333;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
background: #e8e8e8;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
background: #1e1e1e;
|
||||||
|
color: #d4d4d4;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: 'Consolas', 'Courier New', monospace;
|
||||||
|
font-size: 0.85em;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 15px 0;
|
||||||
|
background: white;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
th, td {
|
||||||
|
padding: 10px 12px;
|
||||||
|
text-align: left;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
background: #0078d4;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
tr:nth-child(even) { background: #f9f9f9; }
|
||||||
|
tr:hover { background: #f0f7ff; }
|
||||||
|
hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #ddd;
|
||||||
|
margin: 30px 0;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
background: #fff3cd;
|
||||||
|
border-left: 4px solid #ffc107;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
.info {
|
||||||
|
background: #e7f3ff;
|
||||||
|
border-left: 4px solid #0078d4;
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
ol, ul { padding-left: 25px; }
|
||||||
|
li { margin: 5px 0; }
|
||||||
|
.toc {
|
||||||
|
background: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
.toc h3 { margin-top: 0; }
|
||||||
|
.toc ul { list-style: none; padding-left: 0; }
|
||||||
|
.toc li { margin: 8px 0; }
|
||||||
|
.toc a { color: #0078d4; text-decoration: none; }
|
||||||
|
.toc a:hover { text-decoration: underline; }
|
||||||
|
.comment { color: #6a9955; }
|
||||||
|
.string { color: #ce9178; }
|
||||||
|
.keyword { color: #569cd6; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<h1>WinRM Setup Package for Shopfloor PCs</h1>
|
||||||
|
|
||||||
|
<p>This package provides scripts to configure WinRM (Windows Remote Management) on shopfloor PCs and execute remote maintenance tasks.</p>
|
||||||
|
|
||||||
|
<div class="toc">
|
||||||
|
<h3>Contents</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="#quick-start">Quick Start</a></li>
|
||||||
|
<li><a href="#setup-details">Setup Script Details</a></li>
|
||||||
|
<li><a href="#tasks">Available Remote Tasks</a></li>
|
||||||
|
<li><a href="#troubleshooting">Troubleshooting</a></li>
|
||||||
|
<li><a href="#security">Security Considerations</a></li>
|
||||||
|
<li><a href="#custom-tasks">Adding Custom Tasks</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>Package Contents</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>File</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>Setup-WinRM.bat</code></td><td>Run on each PC to enable and configure WinRM</td></tr>
|
||||||
|
<tr><td><code>Invoke-RemoteTask.ps1</code></td><td>PowerShell script to execute tasks on remote PCs</td></tr>
|
||||||
|
<tr><td><code>hosts.txt</code></td><td>List of target computers (edit before use)</td></tr>
|
||||||
|
<tr><td><code>WinRM-Setup-Guide.html</code></td><td>This documentation</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 id="quick-start">Quick Start</h2>
|
||||||
|
|
||||||
|
<h3>Step 1: Configure Your Admin Workstation</h3>
|
||||||
|
|
||||||
|
<p>Before connecting to remote PCs, run this <strong>once</strong> on your admin workstation (as Administrator):</p>
|
||||||
|
|
||||||
|
<pre><code>Set-Item WSMan:\localhost\Client\TrustedHosts -Value <span class="string">"*.logon.ds.ge.com"</span> -Force</code></pre>
|
||||||
|
|
||||||
|
<p>This allows your workstation to connect to any PC in the domain.</p>
|
||||||
|
|
||||||
|
<h3>Step 2: Configure the Setup Script</h3>
|
||||||
|
|
||||||
|
<p>Edit <code>Setup-WinRM.bat</code> and update these values at the top:</p>
|
||||||
|
|
||||||
|
<pre><code><span class="comment">REM Default security group - who can use WinRM to connect</span>
|
||||||
|
set "DEFAULT_SECURITY_GROUP=<span class="string">logon\groupid</span>"
|
||||||
|
|
||||||
|
<span class="comment">REM Where to log the inventory CSV (network share recommended)</span>
|
||||||
|
set "DEFAULT_LOG_PATH=<span class="string">\\server\share\winrm-inventory</span>"
|
||||||
|
|
||||||
|
<span class="comment">REM Domain suffix for TrustedHosts</span>
|
||||||
|
set "TRUSTED_DOMAIN=<span class="string">*.logon.ds.ge.com</span>"
|
||||||
|
|
||||||
|
<span class="comment">REM Optional: Trust a specific subnet (uncomment and set)</span>
|
||||||
|
<span class="comment">REM set "TRUSTED_SUBNET=10.48.130.*"</span></code></pre>
|
||||||
|
|
||||||
|
<h3>Step 3: Create Security Group in Active Directory</h3>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Open <strong>Active Directory Users and Computers</strong></li>
|
||||||
|
<li>Create a new Security Group (or use existing group matching <code>groupid</code>)</li>
|
||||||
|
<li>Add users who should have remote management access</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>Step 4: Run Setup on Each Shopfloor PC</h3>
|
||||||
|
|
||||||
|
<p>Run as Administrator on each PC:</p>
|
||||||
|
|
||||||
|
<pre><code>Setup-WinRM.bat</code></pre>
|
||||||
|
|
||||||
|
<p>Or with parameters:</p>
|
||||||
|
|
||||||
|
<pre><code>Setup-WinRM.bat "logon\groupid" "\\server\share\winrm-inventory"</code></pre>
|
||||||
|
|
||||||
|
<p>The script will:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Enable WinRM service</li>
|
||||||
|
<li>Configure authentication (Negotiate/Kerberos)</li>
|
||||||
|
<li>Set firewall rules (domain profile only)</li>
|
||||||
|
<li>Restrict access to the security group</li>
|
||||||
|
<li>Log hostname/IP to CSV inventory</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Step 5: Run Remote Tasks</h3>
|
||||||
|
|
||||||
|
<p>From your admin workstation, edit <code>hosts.txt</code> with target PCs, then:</p>
|
||||||
|
|
||||||
|
<pre><code><span class="comment"># Test connectivity</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task TestConnection
|
||||||
|
|
||||||
|
<span class="comment"># Restart print spooler on all hosts</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
|
||||||
|
|
||||||
|
<span class="comment"># Check disk space</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace
|
||||||
|
|
||||||
|
<span class="comment"># Run on a single PC</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName <span class="string">"PC001"</span> -Task FlushDNS</code></pre>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 id="setup-details">Setup Script Details</h2>
|
||||||
|
|
||||||
|
<h3>What Setup-WinRM.bat Configures</h3>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Setting</th><th>Value</th><th>Purpose</th></tr>
|
||||||
|
<tr><td>WinRM Service</td><td>Auto-start</td><td>Ensures WinRM starts on boot</td></tr>
|
||||||
|
<tr><td>AllowUnencrypted</td><td>false</td><td>Security: require encrypted connections</td></tr>
|
||||||
|
<tr><td>Negotiate Auth</td><td>true</td><td>Enables Kerberos/NTLM authentication</td></tr>
|
||||||
|
<tr><td>CredSSP Auth</td><td>true</td><td>Enables credential delegation (double-hop)</td></tr>
|
||||||
|
<tr><td>Firewall</td><td>Domain profile</td><td>Opens port 5985 for domain connections only</td></tr>
|
||||||
|
<tr><td>TrustedHosts</td><td>*.logon.ds.ge.com</td><td>Trusts domain-joined PCs</td></tr>
|
||||||
|
<tr><td>RootSDDL</td><td>Security group</td><td>Restricts who can connect</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>CSV Inventory</h3>
|
||||||
|
|
||||||
|
<p>The setup script logs each PC to a CSV file:</p>
|
||||||
|
|
||||||
|
<pre><code>Hostname,IPAddress,SetupDate,OSVersion,SecurityGroup
|
||||||
|
PC001,10.48.130.101,2026-01-08 09:30:00,10.0,logon\groupid
|
||||||
|
PC002,10.48.130.102,2026-01-08 09:35:00,10.0,logon\groupid</code></pre>
|
||||||
|
|
||||||
|
<p>You can use this CSV as your hosts file:</p>
|
||||||
|
|
||||||
|
<pre><code><span class="comment"># Extract hostnames from CSV</span>
|
||||||
|
Import-Csv <span class="string">"\\server\share\winrm-inventory\winrm-inventory.csv"</span> |
|
||||||
|
Select-Object -ExpandProperty Hostname |
|
||||||
|
Set-Content .\hosts.txt</code></pre>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 id="tasks">Available Remote Tasks</h2>
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr><th>Task</th><th>Description</th></tr>
|
||||||
|
<tr><td><code>TestConnection</code></td><td>Verify WinRM connectivity</td></tr>
|
||||||
|
<tr><td><code>GetUptime</code></td><td>Show system uptime and last boot time</td></tr>
|
||||||
|
<tr><td><code>GetDiskSpace</code></td><td>Show free space on all drives</td></tr>
|
||||||
|
<tr><td><code>RestartSpooler</code></td><td>Restart Print Spooler service</td></tr>
|
||||||
|
<tr><td><code>FlushDNS</code></td><td>Clear DNS resolver cache</td></tr>
|
||||||
|
<tr><td><code>ClearTempFiles</code></td><td>Delete Windows temp files</td></tr>
|
||||||
|
<tr><td><code>DiskCleanup</code></td><td>Run Windows Disk Cleanup</td></tr>
|
||||||
|
<tr><td><code>OptimizeDisk</code></td><td>TRIM (SSD) or Defrag (HDD)</td></tr>
|
||||||
|
<tr><td><code>SyncTime</code></td><td>Force time sync with domain controller</td></tr>
|
||||||
|
<tr><td><code>RestartService</code></td><td>Restart any Windows service (requires <code>-ServiceName</code>)</td></tr>
|
||||||
|
<tr><td><code>RunCommand</code></td><td>Run custom PowerShell command (requires <code>-Command</code>)</td></tr>
|
||||||
|
<tr><td><code>RestartComputer</code></td><td>Restart the remote PC (requires YES confirmation)</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3>Examples</h3>
|
||||||
|
|
||||||
|
<pre><code><span class="comment"># Check uptime on all hosts</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task GetUptime
|
||||||
|
|
||||||
|
<span class="comment"># Restart a specific service</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartService -ServiceName <span class="string">"Spooler"</span>
|
||||||
|
|
||||||
|
<span class="comment"># Run custom command</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RunCommand -Command <span class="string">"Get-Process | Sort CPU -Desc | Select -First 5"</span>
|
||||||
|
|
||||||
|
<span class="comment"># Use custom hosts file</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -HostsFile <span class="string">".\cnc-machines.txt"</span> -Task FlushDNS
|
||||||
|
|
||||||
|
<span class="comment"># Specify DNS suffix for short hostnames</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -DnsSuffix <span class="string">"logon.ds.ge.com"</span> -Task TestConnection
|
||||||
|
|
||||||
|
<span class="comment"># Restart a remote PC (will prompt for confirmation)</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName <span class="string">"PC001"</span> -Task RestartComputer
|
||||||
|
|
||||||
|
<span class="comment"># Increase parallelism for faster execution on many PCs</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task FlushDNS -ThrottleLimit 20
|
||||||
|
|
||||||
|
<span class="comment"># Save results to a log file</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task GetDiskSpace -LogResults</code></pre>
|
||||||
|
|
||||||
|
<h3>Targeting Multiple PCs</h3>
|
||||||
|
|
||||||
|
<pre><code><span class="comment"># Comma-separated list</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName <span class="string">"PC001"</span>,<span class="string">"PC002"</span>,<span class="string">"PC003"</span> -Task GetUptime
|
||||||
|
|
||||||
|
<span class="comment"># Array variable</span>
|
||||||
|
$pcs = @(<span class="string">"PC001"</span>, <span class="string">"PC002"</span>, <span class="string">"PC003"</span>)
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task FlushDNS
|
||||||
|
|
||||||
|
<span class="comment"># From hosts.txt file (default)</span>
|
||||||
|
.\Invoke-RemoteTask.ps1 -Task RestartSpooler
|
||||||
|
|
||||||
|
<span class="comment"># From CSV inventory</span>
|
||||||
|
$pcs = (Import-Csv <span class="string">"\\server\share\winrm-inventory.csv"</span>).Hostname
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task GetDiskSpace
|
||||||
|
|
||||||
|
<span class="comment"># From Active Directory query</span>
|
||||||
|
$pcs = (Get-ADComputer -Filter <span class="string">"Name -like 'SHOPFLOOR-*'"</span>).Name
|
||||||
|
.\Invoke-RemoteTask.ps1 -ComputerName $pcs -Task SyncTime</code></pre>
|
||||||
|
|
||||||
|
<div class="info">
|
||||||
|
<strong>Parallel Execution:</strong> All commands run in parallel (default: 10 concurrent connections). Adjust with <code>-ThrottleLimit</code> parameter.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Logging Results</h3>
|
||||||
|
|
||||||
|
<p>Use <code>-LogResults</code> to save task output to a timestamped log file in the script directory:</p>
|
||||||
|
|
||||||
|
<pre><code>.\Invoke-RemoteTask.ps1 -Task RestartSpooler -LogResults
|
||||||
|
<span class="comment"># Creates: RemoteTask_20260108_143022.log</span></code></pre>
|
||||||
|
|
||||||
|
<p>Log files contain:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Task name and parameters</li>
|
||||||
|
<li>Execution time</li>
|
||||||
|
<li>Status of each computer (OK/FAIL)</li>
|
||||||
|
<li>Result messages</li>
|
||||||
|
<li>Summary totals</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 id="troubleshooting">Troubleshooting</h2>
|
||||||
|
|
||||||
|
<h3>"Access Denied" when connecting</h3>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Verify you're a member of the WinRM security group</li>
|
||||||
|
<li>Check that your credentials are correct</li>
|
||||||
|
<li>Verify the target PC ran Setup-WinRM.bat successfully</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>"WinRM cannot complete the operation"</h3>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li>Verify the target PC is reachable: <code>ping PC001</code></li>
|
||||||
|
<li>Check WinRM is running on target: <code>sc query winrm</code> (on target PC)</li>
|
||||||
|
<li>Verify firewall allows port 5985</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<h3>"The WinRM client cannot process the request"</h3>
|
||||||
|
|
||||||
|
<p>Add target to TrustedHosts on your admin workstation:</p>
|
||||||
|
|
||||||
|
<pre><code>Set-Item WSMan:\localhost\Client\TrustedHosts -Value <span class="string">"*.logon.ds.ge.com"</span> -Force</code></pre>
|
||||||
|
|
||||||
|
<h3>Test WinRM Configuration</h3>
|
||||||
|
|
||||||
|
<p>On your admin workstation:</p>
|
||||||
|
|
||||||
|
<pre><code><span class="comment"># Test basic connectivity</span>
|
||||||
|
Test-WSMan -ComputerName PC001
|
||||||
|
|
||||||
|
<span class="comment"># Test with credentials</span>
|
||||||
|
$cred = Get-Credential
|
||||||
|
Test-WSMan -ComputerName PC001 -Credential $cred -Authentication Negotiate
|
||||||
|
|
||||||
|
<span class="comment"># Enter interactive session</span>
|
||||||
|
Enter-PSSession -ComputerName PC001 -Credential $cred -Authentication Negotiate</code></pre>
|
||||||
|
|
||||||
|
<p>On the target PC:</p>
|
||||||
|
|
||||||
|
<pre><code>winrm enumerate winrm/config/listener
|
||||||
|
winrm get winrm/config/service</code></pre>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 id="security">Security Considerations</h2>
|
||||||
|
|
||||||
|
<div class="warning">
|
||||||
|
<strong>Important Security Notes:</strong>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol>
|
||||||
|
<li><strong>Use Security Groups</strong>: Always restrict WinRM access to a specific AD group</li>
|
||||||
|
<li><strong>Domain Profile Only</strong>: Firewall rules only allow connections on domain networks</li>
|
||||||
|
<li><strong>No Unencrypted Traffic</strong>: AllowUnencrypted is set to false</li>
|
||||||
|
<li><strong>Audit Access</strong>: Enable Windows Security auditing for logon events</li>
|
||||||
|
<li><strong>Credential Protection</strong>: Use dedicated admin accounts, not personal accounts</li>
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2 id="custom-tasks">Adding Custom Tasks</h2>
|
||||||
|
|
||||||
|
<p>Edit <code>Invoke-RemoteTask.ps1</code> and add to the <code>$TaskScripts</code> hashtable:</p>
|
||||||
|
|
||||||
|
<pre><code><span class="string">'MyCustomTask'</span> = {
|
||||||
|
$result = @{
|
||||||
|
Success = <span class="keyword">$false</span>
|
||||||
|
Hostname = $env:COMPUTERNAME
|
||||||
|
Output = <span class="string">""</span>
|
||||||
|
Error = <span class="keyword">$null</span>
|
||||||
|
}
|
||||||
|
<span class="keyword">try</span> {
|
||||||
|
<span class="comment"># Your code here</span>
|
||||||
|
$result.Output = <span class="string">"Task completed"</span>
|
||||||
|
$result.Success = <span class="keyword">$true</span>
|
||||||
|
} <span class="keyword">catch</span> {
|
||||||
|
$result.Error = $_.Exception.Message
|
||||||
|
}
|
||||||
|
<span class="keyword">return</span> $result
|
||||||
|
}</code></pre>
|
||||||
|
|
||||||
|
<p>Then add the task name to the <code>ValidateSet</code> in the param block.</p>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<h2>Support</h2>
|
||||||
|
|
||||||
|
<p>For issues or questions, contact your IT support team.</p>
|
||||||
|
|
||||||
|
<p style="text-align: center; color: #666; margin-top: 40px; font-size: 0.9em;">
|
||||||
|
WinRM Setup Package © 2026 | Generated from README.md
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
16
winrm-setup-package/hosts.txt
Normal file
16
winrm-setup-package/hosts.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# WinRM Target Hosts
|
||||||
|
# -------------------
|
||||||
|
# Add one hostname or IP address per line
|
||||||
|
# Lines starting with # are comments
|
||||||
|
#
|
||||||
|
# You can use:
|
||||||
|
# - Hostnames: PC001, SHOPFLOOR-PC-01
|
||||||
|
# - FQDNs: PC001.logon.ds.ge.com
|
||||||
|
# - IP addresses: 10.48.130.100
|
||||||
|
#
|
||||||
|
# Example:
|
||||||
|
# PC001
|
||||||
|
# PC002
|
||||||
|
# 10.48.130.100
|
||||||
|
# shopfloor-cnc-01.logon.ds.ge.com
|
||||||
|
|
||||||
Reference in New Issue
Block a user