Compare commits

...

5 Commits

Author SHA1 Message Date
068888202c Quote wizard: fix API URL and suPHP auth header handling
- Change production API URL from /msp-api to /quote/api
- Switch admin auth to X-Api-Key header as primary (suPHP strips Authorization)
- Keep Bearer token as fallback for PHP-FPM environments

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:08:32 -07:00
6c2c693e6d Dataforth: Fix DEPLOY.BAT trailing space bug, session log update
DEPLOY v4.1 fixes critical bug where ECHO >> redirects included
trailing space in MACHINE variable, causing "Too many parameters"
on all COPY commands with subdirectory paths. TS-4L data upload
confirmed working - 84 test files + 90 reports on NAS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:08:32 -07:00
78528d545e Fix DOS batch files: remove IF EXIST checks causing failures
DOS 6.22 IF EXIST with wildcards on SMB1 network paths causes
"Bad command" and "Too many parameters" errors. Rewrote CTONW,
NWTOC, and AUTOEXEC to v4.0 with direct COPY/MD commands.
Pre-created all station LOGS directories on new D2TESTNAS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:08:32 -07:00
000ee3da5c Session log: D2TESTNAS VM build, NAS migration, rsync sync fix
Built Debian 13 VM replacement for aging ReadyNAS, deployed rsync-based
sync script to AD2, transferred data, completed IP cutover to 192.168.0.9.
Includes setup scripts, sync fixes, and comprehensive session logs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:08:32 -07:00
470638ff86 sync: Dataforth sync fixes, TestDataDB stability, and client scripts
Dataforth DOS:
- TestDataDB: singleton DB connection fix (crash prevention), WAL mode,
  WinSW service config, backup script, uncaught exception handlers
- Sync-FromNAS.ps1: Get-NASFileList temp file approach to avoid SSH
  stdout deadlock, *> $null output suppression, 8.3 filename filter
  for PUSH phase, backslash-escaped SCP paths, rename-to-.synced
- import.js: INSERT OR REPLACE for re-tested devices
- Full import run: 1,028,275 -> 1,632,793 records, indexes added
- Deploy script for sync fixes to AD2

Client scripts (temp/):
- BG Builders: Lesley account check, MFA phone update
- Lonestar Electrical: Kyla/Russ Google Workspace setup, 2FA bypass
- AD2 diagnostics and NAS connectivity tests

PENDING: Investigate why newest test_date is Jan 19 despite daily tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 06:08:31 -07:00
35 changed files with 5020 additions and 193 deletions

View File

@@ -1,8 +1,7 @@
@ECHO OFF
REM Dataforth Test Machine Startup - DOS 6.22
REM Automatically runs after CONFIG.SYS during boot
REM Version: 3.0 - Auto-update system integrated
REM Last modified: 2026-01-19
REM Version: 4.0 - No IF EXIST checks
REM Last modified: 2026-03-12
REM Set machine identity (configured by DEPLOY.BAT)
SET MACHINE=TS-4R
@@ -21,25 +20,21 @@ CLS
ECHO.
ECHO ==============================================================
ECHO Dataforth Test Machine: %MACHINE%
ECHO DOS 6.22 with Automatic Update System
ECHO AUTOEXEC v4.0 - 2026-03-12
ECHO ==============================================================
ECHO.
REM Create required directories if they don't exist
IF NOT EXIST C:\TEMP\*.* MD C:\TEMP
IF NOT EXIST C:\BAT\*.* MD C:\BAT
IF NOT EXIST C:\BATCH\*.* MD C:\BATCH
REM Create required directories
MD C:\TEMP
MD C:\BAT
MD C:\BATCH
ECHO Starting network client...
ECHO.
REM Start network client and map T: and X: drives
IF EXIST C:\STARTNET.BAT CALL C:\STARTNET.BAT
CALL C:\STARTNET.BAT
REM Verify T: drive is accessible
IF NOT EXIST T:\*.* GOTO NET_FAILED
ECHO (OK) Network started
ECHO.
ECHO Network Drives:
ECHO T: = \\D2TESTNAS\test
@@ -48,11 +43,11 @@ ECHO.
REM Download latest software updates from network
ECHO Checking for software updates...
IF EXIST C:\BAT\NWTOC.BAT CALL C:\BAT\NWTOC.BAT
CALL C:\BAT\NWTOC.BAT
REM Upload test data to network for database import
ECHO Uploading test data to network...
IF EXIST C:\BAT\CTONW.BAT CALL C:\BAT\CTONW.BAT
CALL C:\BAT\CTONW.BAT
ECHO.
ECHO ==============================================================
@@ -65,17 +60,5 @@ ECHO CHECKUPD - Check for available updates
ECHO CTONW - Manual upload to network
ECHO NWTOC - Manual download from network
ECHO.
GOTO END
:NET_FAILED
ECHO ERROR: Network drive mapping failed
ECHO T: drive not accessible
ECHO.
ECHO To start network manually:
ECHO C:\STARTNET.BAT
ECHO.
ECHO Updates and backups will not work until network is available.
ECHO.
PAUSE
:END

View File

@@ -1,62 +1,37 @@
@ECHO OFF
REM Computer to Network - Upload local test results to network
REM Version: 3.2 - DOS 6.22 compatible
REM Last modified: 2026-01-21
REM Version: 4.0 - No IF EXIST checks, dirs pre-created on server
REM Last modified: 2026-03-12
REM Verify MACHINE variable is set
IF "%MACHINE%"=="" GOTO NO_MACHINE
REM Verify T: drive
IF NOT EXIST T:\*.* GOTO NO_DRIVE
REM Verify machine folder exists
IF NOT EXIST T:\%MACHINE%\*.* GOTO NO_FOLDER
ECHO ........................................
ECHO Archiving datalog files to network...
ECHO CTONW.BAT v3.2 > C:\ATE\CTONW.LOG
ECHO CTONW.BAT v4.0 > C:\ATE\CTONW.LOG
ECHO Machine: %MACHINE% >> C:\ATE\CTONW.LOG
ECHO Copying from C:\ATE\ to T:\%MACHINE%\LOGS\ >> C:\ATE\CTONW.LOG
REM Check for ATE directory
IF NOT EXIST C:\ATE\*.* GOTO SKIP_ATE
REM Ensure target LOGS directories exist
IF NOT EXIST T:\%MACHINE%\LOGS\*.* MD T:\%MACHINE%\LOGS
IF NOT EXIST T:\%MACHINE%\LOGS\5BLOG\*.* MD T:\%MACHINE%\LOGS\5BLOG
IF NOT EXIST T:\%MACHINE%\LOGS\7BLOG\*.* MD T:\%MACHINE%\LOGS\7BLOG
IF NOT EXIST T:\%MACHINE%\LOGS\8BLOG\*.* MD T:\%MACHINE%\LOGS\8BLOG
IF NOT EXIST T:\%MACHINE%\LOGS\DSCLOG\*.* MD T:\%MACHINE%\LOGS\DSCLOG
IF NOT EXIST T:\%MACHINE%\LOGS\HVLOG\*.* MD T:\%MACHINE%\LOGS\HVLOG
IF NOT EXIST T:\%MACHINE%\LOGS\PWRLOG\*.* MD T:\%MACHINE%\LOGS\PWRLOG
IF NOT EXIST T:\%MACHINE%\LOGS\SCTLOG\*.* MD T:\%MACHINE%\LOGS\SCTLOG
IF NOT EXIST T:\%MACHINE%\LOGS\VASLOG\*.* MD T:\%MACHINE%\LOGS\VASLOG
IF EXIST C:\ATE\5BLOG\*.DAT COPY C:\ATE\5BLOG\*.DAT T:\%MACHINE%\LOGS\5BLOG
IF EXIST C:\ATE\7BLOG\*.DAT COPY C:\ATE\7BLOG\*.DAT T:\%MACHINE%\LOGS\7BLOG
IF EXIST C:\ATE\7BLOG\*.SHT COPY C:\ATE\7BLOG\*.SHT T:\%MACHINE%\LOGS\7BLOG
IF EXIST C:\ATE\8BLOG\*.DAT COPY C:\ATE\8BLOG\*.DAT T:\%MACHINE%\LOGS\8BLOG
IF EXIST C:\ATE\DSCLOG\*.DAT COPY C:\ATE\DSCLOG\*.DAT T:\%MACHINE%\LOGS\DSCLOG
IF EXIST C:\ATE\HVLOG\*.DAT COPY C:\ATE\HVLOG\*.DAT T:\%MACHINE%\LOGS\HVLOG
IF EXIST C:\ATE\PWRLOG\*.DAT COPY C:\ATE\PWRLOG\*.DAT T:\%MACHINE%\LOGS\PWRLOG
IF EXIST C:\ATE\SCTLOG\*.DAT COPY C:\ATE\SCTLOG\*.DAT T:\%MACHINE%\LOGS\SCTLOG
IF EXIST C:\ATE\VASLOG\*.DAT COPY C:\ATE\VASLOG\*.DAT T:\%MACHINE%\LOGS\VASLOG
REM Copy log data to network (dirs pre-created on server)
COPY C:\ATE\5BLOG\*.DAT T:\%MACHINE%\LOGS\5BLOG
COPY C:\ATE\7BLOG\*.DAT T:\%MACHINE%\LOGS\7BLOG
COPY C:\ATE\7BLOG\*.SHT T:\%MACHINE%\LOGS\7BLOG
COPY C:\ATE\8BLOG\*.DAT T:\%MACHINE%\LOGS\8BLOG
COPY C:\ATE\DSCLOG\*.DAT T:\%MACHINE%\LOGS\DSCLOG
COPY C:\ATE\HVLOG\*.DAT T:\%MACHINE%\LOGS\HVLOG
COPY C:\ATE\PWRLOG\*.DAT T:\%MACHINE%\LOGS\PWRLOG
COPY C:\ATE\SCTLOG\*.DAT T:\%MACHINE%\LOGS\SCTLOG
COPY C:\ATE\VASLOG\*.DAT T:\%MACHINE%\LOGS\VASLOG
ECHO Archiving work-order report files to network...
IF NOT EXIST T:\%MACHINE%\Reports\*.* MD T:\%MACHINE%\Reports
IF EXIST C:\Reports\*.TXT COPY C:\Reports\*.TXT T:\%MACHINE%\Reports
COPY C:\Reports\*.TXT T:\%MACHINE%\Reports
ECHO Archiving log file to network...
IF EXIST C:\ATE\*.LOG COPY C:\ATE\*.LOG T:\%MACHINE%
COPY C:\ATE\*.LOG T:\%MACHINE%
ECHO Network archiving of datalog files done!
ECHO ........................................
GOTO END
:SKIP_ATE
ECHO No C:\ATE directory - skipping
GOTO END
:NO_MACHINE
ECHO ........................................
ECHO ERROR: MACHINE variable not set
@@ -65,18 +40,4 @@ ECHO ........................................
PAUSE
GOTO END
:NO_DRIVE
ECHO ERROR: T: drive not available
ECHO Run C:\STARTNET.BAT first
PAUSE
GOTO END
:NO_FOLDER
ECHO ........................................
ECHO ERROR: Machine folder T:\%MACHINE% not found
ECHO Run ATESYNC to create it first
ECHO ........................................
PAUSE
GOTO END
:END

View File

@@ -1,8 +1,8 @@
@ECHO OFF
REM One-time deployment script for DOS Update System
REM Usage: T:\COMMON\ProdSW\DEPLOY.BAT machine-name
REM Version: 2.4 - Use COPY instead of XCOPY (DOS 6.22 compatibility)
REM Last modified: 2026-01-21
REM Version: 4.1 - Fixed trailing space bug in ECHO >> redirects
REM Last modified: 2026-03-12
CLS
@@ -13,7 +13,7 @@ REM Save machine name
SET MACHINE=%1
ECHO ==============================================================
ECHO DOS Update System - Deployment
ECHO DOS Update System - Deployment v4.1
ECHO ==============================================================
ECHO Machine: %MACHINE%
ECHO ==============================================================
@@ -22,52 +22,89 @@ ECHO Press any key to install...
PAUSE >NUL
ECHO.
REM Create directories (ignore errors with >NUL)
REM Create local directories
MD C:\BAT >NUL
MD T:\%MACHINE% >NUL
MD C:\BATCH >NUL
MD C:\TEMP >NUL
MD C:\ATE >NUL
MD C:\NET >NUL
ECHO (1/2) Copying batch files to C:\BAT...
COPY T:\COMMON\ProdSW\NWTOC.BAT C:\BAT >NUL
COPY T:\COMMON\ProdSW\CTONW.BAT C:\BAT >NUL
COPY T:\COMMON\ProdSW\UPDATE.BAT C:\BAT >NUL
COPY T:\COMMON\ProdSW\CHECKUPD.BAT C:\BAT >NUL
COPY T:\COMMON\ProdSW\STAGE.BAT C:\BAT >NUL
COPY T:\COMMON\ProdSW\REBOOT.BAT C:\BAT >NUL
ECHO (1/3) Copying batch files to C:\BAT...
COPY T:\COMMON\ProdSW\*.BAT C:\BAT >NUL
ECHO Batch files installed
ECHO.
ECHO (2/2) Installing AUTOEXEC.BAT...
REM Create AUTOEXEC.BAT with machine name
ECHO @ECHO OFF > C:\AUTOEXEC.BAT
ECHO REM Dataforth Test Machine - DOS 6.22 >> C:\AUTOEXEC.BAT
ECHO SET MACHINE=%MACHINE% >> C:\AUTOEXEC.BAT
ECHO SET PATH=C:\DOS;C:\NET;C:\BAT;C:\BATCH;C:\ >> C:\AUTOEXEC.BAT
ECHO PROMPT $P$G >> C:\AUTOEXEC.BAT
ECHO SET TEMP=C:\TEMP >> C:\AUTOEXEC.BAT
ECHO SET TMP=C:\TEMP >> C:\AUTOEXEC.BAT
ECHO MD C:\TEMP >NUL >> C:\AUTOEXEC.BAT
ECHO CLS >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO ECHO Dataforth Test Machine: %MACHINE% >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO IF EXIST C:\STARTNET.BAT CALL C:\STARTNET.BAT >> C:\AUTOEXEC.BAT
ECHO IF NOT EXIST T:\*.* GOTO NONET >> C:\AUTOEXEC.BAT
ECHO IF EXIST C:\BAT\NWTOC.BAT CALL C:\BAT\NWTOC.BAT >> C:\AUTOEXEC.BAT
ECHO IF EXIST C:\BAT\CTONW.BAT CALL C:\BAT\CTONW.BAT >> C:\AUTOEXEC.BAT
ECHO GOTO READY >> C:\AUTOEXEC.BAT
ECHO :NONET >> C:\AUTOEXEC.BAT
ECHO ECHO ERROR: Network not available >> C:\AUTOEXEC.BAT
ECHO :READY >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO ECHO System Ready >> C:\AUTOEXEC.BAT
ECHO ECHO. >> C:\AUTOEXEC.BAT
ECHO CD \ATE >> C:\AUTOEXEC.BAT
ECHO menux >> C:\AUTOEXEC.BAT
ECHO (2/3) Creating machine directory on network...
MD T:\%MACHINE% >NUL
MD T:\%MACHINE%\LOGS >NUL
MD T:\%MACHINE%\LOGS\5BLOG >NUL
MD T:\%MACHINE%\LOGS\7BLOG >NUL
MD T:\%MACHINE%\LOGS\8BLOG >NUL
MD T:\%MACHINE%\LOGS\DSCLOG >NUL
MD T:\%MACHINE%\LOGS\HVLOG >NUL
MD T:\%MACHINE%\LOGS\PWRLOG >NUL
MD T:\%MACHINE%\LOGS\SCTLOG >NUL
MD T:\%MACHINE%\LOGS\VASLOG >NUL
MD T:\%MACHINE%\Reports >NUL
MD T:\%MACHINE%\BACKUP >NUL
ECHO Network directories created
ECHO.
ECHO (3/3) Installing AUTOEXEC.BAT...
REM Create AUTOEXEC.BAT with machine name - no IF EXIST checks
REM NOTE: Redirect BEFORE echo to avoid trailing spaces in output
REM First line uses > to overwrite, rest use >> to append
>C:\AUTOEXEC.BAT ECHO @ECHO OFF
>>C:\AUTOEXEC.BAT ECHO REM Dataforth Test Machine Startup - DOS 6.22
>>C:\AUTOEXEC.BAT ECHO REM Version: 4.1 - No IF EXIST checks
>>C:\AUTOEXEC.BAT ECHO REM Deployed: 2026-03-12
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO SET MACHINE=%MACHINE%
>>C:\AUTOEXEC.BAT ECHO SET PATH=C:\DOS;C:\NET;C:\BAT;C:\BATCH;C:\
>>C:\AUTOEXEC.BAT ECHO PROMPT $P$G
>>C:\AUTOEXEC.BAT ECHO SET TEMP=C:\TEMP
>>C:\AUTOEXEC.BAT ECHO SET TMP=C:\TEMP
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO CLS
>>C:\AUTOEXEC.BAT ECHO ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO ==============================================================
>>C:\AUTOEXEC.BAT ECHO ECHO Dataforth Test Machine: %MACHINE%
>>C:\AUTOEXEC.BAT ECHO ECHO AUTOEXEC v4.1 - 2026-03-12
>>C:\AUTOEXEC.BAT ECHO ECHO ==============================================================
>>C:\AUTOEXEC.BAT ECHO ECHO.
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO MD C:\TEMP >NUL
>>C:\AUTOEXEC.BAT ECHO MD C:\BAT >NUL
>>C:\AUTOEXEC.BAT ECHO MD C:\BATCH >NUL
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO Starting network client...
>>C:\AUTOEXEC.BAT ECHO ECHO.
>>C:\AUTOEXEC.BAT ECHO CALL C:\STARTNET.BAT
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO Network Drives:
>>C:\AUTOEXEC.BAT ECHO ECHO T: = \\D2TESTNAS\test
>>C:\AUTOEXEC.BAT ECHO ECHO X: = \\D2TESTNAS\datasheets
>>C:\AUTOEXEC.BAT ECHO ECHO.
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO Checking for software updates...
>>C:\AUTOEXEC.BAT ECHO CALL C:\BAT\NWTOC.BAT
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO Uploading test data to network...
>>C:\AUTOEXEC.BAT ECHO CALL C:\BAT\CTONW.BAT
>>C:\AUTOEXEC.BAT ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO.
>>C:\AUTOEXEC.BAT ECHO ECHO ==============================================================
>>C:\AUTOEXEC.BAT ECHO ECHO System Ready
>>C:\AUTOEXEC.BAT ECHO ECHO ==============================================================
>>C:\AUTOEXEC.BAT ECHO ECHO.
>>C:\AUTOEXEC.BAT ECHO CD \ATE
>>C:\AUTOEXEC.BAT ECHO menux
ECHO AUTOEXEC.BAT installed with MACHINE=%MACHINE%
ECHO.
ECHO ==============================================================
ECHO Deployment Complete - REBOOT NOW
ECHO Deployment Complete v4.1 - REBOOT NOW
ECHO ==============================================================
ECHO.
PAUSE

View File

@@ -1,80 +1,49 @@
@ECHO OFF
REM Network to Computer - Download software updates from network to local C: drive
REM Version: 3.5 - Use COPY, no NUL redirects
REM Last modified: 2026-01-21
REM Network to Computer - Download software updates from network
REM Version: 4.0 - No IF EXIST checks, direct COPY
REM Last modified: 2026-03-12
REM Check T: drive
IF NOT EXIST T:\*.* GOTO NO_DRIVE
REM Display banner
ECHO.
ECHO ==============================================================
ECHO Download Updates from Network
ECHO NWTOC v4.0 - Download Updates from Network
ECHO ==============================================================
ECHO.
REM Create local directories (errors ignored)
IF NOT EXIST C:\BAT\*.* MD C:\BAT
IF NOT EXIST C:\ATE\*.* MD C:\ATE
IF NOT EXIST C:\ATE\5BDATA\*.* MD C:\ATE\5BDATA
IF NOT EXIST C:\ATE\7BDATA\*.* MD C:\ATE\7BDATA
IF NOT EXIST C:\ATE\8BDATA\*.* MD C:\ATE\8BDATA
IF NOT EXIST C:\ATE\DSCDATA\*.* MD C:\ATE\DSCDATA
IF NOT EXIST C:\ATE\HVDATA\*.* MD C:\ATE\HVDATA
IF NOT EXIST C:\ATE\PWRDATA\*.* MD C:\ATE\PWRDATA
IF NOT EXIST C:\ATE\RMSDATA\*.* MD C:\ATE\RMSDATA
IF NOT EXIST C:\ATE\SCTDATA\*.* MD C:\ATE\SCTDATA
IF NOT EXIST C:\NET\*.* MD C:\NET
REM Check for COMMON updates
IF NOT EXIST T:\COMMON\ProdSW\*.* GOTO NO_COMMON
REM Create local directories (MD is harmless if they exist locally)
MD C:\BAT
MD C:\ATE
MD C:\ATE\5BDATA
MD C:\ATE\7BDATA
MD C:\ATE\8BDATA
MD C:\ATE\DSCDATA
MD C:\ATE\HVDATA
MD C:\ATE\PWRDATA
MD C:\ATE\RMSDATA
MD C:\ATE\SCTDATA
MD C:\NET
ECHO (1/3) Copying batch files to C:\BAT...
COPY T:\COMMON\ProdSW\*.BAT C:\BAT
ECHO Done
ECHO.
ECHO (2/3) Copying ATE data folders to C:\ATE...
IF EXIST T:\Ate\ProdSW\5BDATA\*.* COPY T:\Ate\ProdSW\5BDATA\*.* C:\ATE\5BDATA
IF EXIST T:\Ate\ProdSW\7BDATA\*.* COPY T:\Ate\ProdSW\7BDATA\*.* C:\ATE\7BDATA
IF EXIST T:\Ate\ProdSW\8BDATA\*.* COPY T:\Ate\ProdSW\8BDATA\*.* C:\ATE\8BDATA
IF EXIST T:\Ate\ProdSW\DSCDATA\*.* COPY T:\Ate\ProdSW\DSCDATA\*.* C:\ATE\DSCDATA
IF EXIST T:\Ate\ProdSW\HVDATA\*.* COPY T:\Ate\ProdSW\HVDATA\*.* C:\ATE\HVDATA
IF EXIST T:\Ate\ProdSW\PWRDATA\*.* COPY T:\Ate\ProdSW\PWRDATA\*.* C:\ATE\PWRDATA
IF EXIST T:\Ate\ProdSW\RMSDATA\*.* COPY T:\Ate\ProdSW\RMSDATA\*.* C:\ATE\RMSDATA
IF EXIST T:\Ate\ProdSW\SCTDATA\*.* COPY T:\Ate\ProdSW\SCTDATA\*.* C:\ATE\SCTDATA
ECHO Done
COPY T:\Ate\ProdSW\5BDATA\*.* C:\ATE\5BDATA
COPY T:\Ate\ProdSW\7BDATA\*.* C:\ATE\7BDATA
COPY T:\Ate\ProdSW\8BDATA\*.* C:\ATE\8BDATA
COPY T:\Ate\ProdSW\DSCDATA\*.* C:\ATE\DSCDATA
COPY T:\Ate\ProdSW\HVDATA\*.* C:\ATE\HVDATA
COPY T:\Ate\ProdSW\PWRDATA\*.* C:\ATE\PWRDATA
COPY T:\Ate\ProdSW\RMSDATA\*.* C:\ATE\RMSDATA
COPY T:\Ate\ProdSW\SCTDATA\*.* C:\ATE\SCTDATA
ECHO.
REM Check for network client updates
IF NOT EXIST T:\COMMON\NET\*.* GOTO SKIP_NET
ECHO (3/3) Copying network files to C:\NET...
COPY T:\COMMON\NET\*.* C:\NET
ECHO Done
ECHO.
GOTO DONE
:SKIP_NET
ECHO (3/3) No network updates
ECHO.
:DONE
ECHO ==============================================================
ECHO Download Complete
ECHO NWTOC v4.0 Download Complete
ECHO ==============================================================
ECHO.
GOTO END
:NO_COMMON
ECHO ERROR: T:\COMMON\ProdSW not found
PAUSE
GOTO END
:NO_DRIVE
ECHO ERROR: T: drive not available
ECHO Run C:\STARTNET.BAT first
PAUSE
GOTO END
:END

View File

@@ -0,0 +1,369 @@
# D2TESTNAS VM Replacement
Replacement for Netgear ReadyNAS RN10400 (D2TESTNAS) used in Dataforth DOS 6.22
test infrastructure. The new system is a Debian 13 (Trixie) VM running on Hyper-V
with BTRFS for snapshots, Samba with SMB1 for DOS compatibility, and rsync daemon
for AD2 bidirectional sync.
---
## 1. Hyper-V VM Creation
Run these PowerShell commands on the Hyper-V host as Administrator.
```powershell
# --- Configuration ---
$VMName = "D2TESTNAS"
$VMPath = "D:\Hyper-V\VMs"
$VHDPath = "D:\Hyper-V\VMs\D2TESTNAS\Virtual Hard Disks"
$ISOPath = "D:\ISOs\debian-13-netinst-amd64.iso" # Download from https://www.debian.org/devel/debian-installer/
$SwitchName = "Dataforth-Bridge" # Your existing vSwitch for 192.168.0.0/24
# --- Create VM ---
New-VM -Name $VMName `
-Path $VMPath `
-MemoryStartupBytes 2GB `
-Generation 2 `
-SwitchName $SwitchName
# --- Configure VM ---
Set-VM -Name $VMName `
-ProcessorCount 2 `
-DynamicMemory `
-MemoryMinimumBytes 1GB `
-MemoryMaximumBytes 4GB `
-AutomaticStartAction Start `
-AutomaticStartDelay 30 `
-AutomaticStopAction ShutDown
# --- Create OS disk (40 GB) ---
New-VHD -Path "$VHDPath\os.vhdx" -SizeBytes 40GB -Dynamic
Add-VMHardDiskDrive -VMName $VMName -Path "$VHDPath\os.vhdx"
# --- Create DATA disk (200 GB, or match current NAS capacity) ---
# This disk will be formatted as BTRFS and mounted at /data
New-VHD -Path "$VHDPath\data.vhdx" -SizeBytes 200GB -Dynamic
Add-VMHardDiskDrive -VMName $VMName -Path "$VHDPath\data.vhdx"
# --- Attach Debian ISO ---
Add-VMDvdDrive -VMName $VMName -Path $ISOPath
# --- Set boot order: DVD first (for install), then disk ---
$dvd = Get-VMDvdDrive -VMName $VMName
$disk = Get-VMHardDiskDrive -VMName $VMName | Where-Object { $_.Path -like "*os.vhdx" }
Set-VMFirmware -VMName $VMName -BootOrder $dvd, $disk
# --- Disable Secure Boot (Debian needs "Microsoft UEFI Certificate Authority") ---
Set-VMFirmware -VMName $VMName -EnableSecureBoot Off
# --- Start VM ---
Start-VM -Name $VMName
vmconnect localhost $VMName
```
Adjust `$SwitchName` to match whatever virtual switch bridges to the 192.168.0.0/24
Dataforth network. If you do not have one, create it:
```powershell
# Create external vSwitch bridged to the physical NIC on the Dataforth network
New-VMSwitch -Name "Dataforth-Bridge" -NetAdapterName "Ethernet 2" -AllowManagementOS $true
```
---
## 2. Debian Installation Notes
During the Debian 13 (Trixie) netinst installation:
1. **Language/Region:** English, United States, UTF-8
2. **Hostname:** `D2TESTNAS`
3. **Domain:** leave blank
4. **Root password:** `Paper123!@#-nas`
5. **User account:** Skip creating a normal user (or create one for admin use)
6. **Partitioning - IMPORTANT:**
- Use "Manual" partitioning
- **Disk 1 (40 GB, /dev/sda):** OS disk
- 512 MB EFI System Partition (ESP)
- 1 GB /boot (ext4)
- Remainder: / (ext4)
- (No swap partition -- Hyper-V dynamic memory handles this; or add 2 GB swap)
- **Disk 2 (200 GB, /dev/sdb):** Data disk
- Use entire disk as a single partition
- **Format as BTRFS**
- Mount point: **/data**
7. **Software selection:**
- Deselect "Debian desktop environment" and all desktop options
- Select "SSH server"
- Select "standard system utilities"
- Do NOT select any web server or print server
8. **GRUB:** Install to /dev/sda
After reboot, verify you can SSH in, then proceed to post-install.
---
## 3. Post-Install Setup
SSH into the new VM (use the DHCP address shown at the console):
```bash
ssh root@<dhcp-ip-address>
```
Transfer and run the setup script:
```bash
# From your workstation (PowerShell/bash):
scp setup-d2testnas.sh root@<dhcp-ip-address>:/root/
# On the VM:
chmod +x /root/setup-d2testnas.sh
/root/setup-d2testnas.sh
```
The script will:
- Install samba, rsync, btrfs-progs, and supporting packages
- Set hostname to D2TESTNAS
- Create BTRFS subvolumes (/data/test, /data/datasheets)
- Write /etc/samba/smb.conf with SMB1 (CORE protocol) support
- Create Samba users ts-1 through ts-50 (null passwords) and engineer
- Write /etc/rsyncd.conf and /etc/rsyncd.secrets
- Install BTRFS snapshot cron jobs
- Configure SSH for root login with password
- Enable and start all services
- Run verification checks
- Display cutover instructions
---
## 4. Testing Before Cutover
While the VM is still on a DHCP address (not 192.168.0.9), verify all services
work. Use the DHCP IP in place of 192.168.0.9 for these tests.
### Test SMB from Windows
```cmd
net use Z: \\<dhcp-ip>\test
dir Z:\
net use Z: /delete
```
### Test rsync from AD2
```powershell
$env:RSYNC_PASSWORD = "IQ203s32119"
rsync --list-only rsync://rsync@<dhcp-ip>/test/
```
### Test SSH
```bash
ssh root@<dhcp-ip>
# Password: Paper123!@#-nas
```
### Test BTRFS Snapshots
```bash
# On the VM:
btrfs-snapshot.sh create hourly
btrfs-snapshot.sh list
ls /data/.snapshots/
```
### Test from DOS Machine (optional, requires temporary IP or hosts hack)
If you can temporarily set a DOS machine to use the DHCP IP, test the T: drive
mapping. Otherwise, wait for cutover.
---
## 5. Data Migration
Before cutover, copy all data from the old NAS to the new VM:
```bash
# On the new VM, pull everything from the old NAS:
RSYNC_PASSWORD=IQ203s32119 rsync -avz --progress \
rsync://rsync@192.168.0.9/test/ /data/test/
```
This may take a while depending on data volume. Run it multiple times -- rsync
is incremental and will only transfer changes on subsequent runs.
For the datasheets share, copy via SMB or SCP from the old NAS:
```bash
# If rsync module exists for datasheets:
RSYNC_PASSWORD=IQ203s32119 rsync -avz rsync://rsync@192.168.0.9/datasheets/ /data/datasheets/
# Otherwise, mount the old NAS share temporarily:
apt-get install -y cifs-utils
mkdir -p /mnt/old-nas
mount -t cifs //192.168.0.9/datasheets /mnt/old-nas -o guest,vers=1.0
rsync -avz /mnt/old-nas/ /data/datasheets/
umount /mnt/old-nas
```
---
## 6. Cutover Checklist
Perform these steps during a maintenance window when no DOS machines are running
tests.
### Pre-Cutover
- [ ] All data migrated from old NAS (run rsync one final time)
- [ ] All services verified on new VM (SMB, rsync, SSH)
- [ ] BTRFS snapshots working (run `btrfs-snapshot.sh list`)
- [ ] Notify engineers: maintenance window, expect brief T: drive outage
### Cutover Steps
1. **Stop the AD2 sync script** (disable scheduled task on AD2 temporarily)
2. **Final data sync** from old NAS to new VM:
```bash
RSYNC_PASSWORD=IQ203s32119 rsync -avz rsync://rsync@192.168.0.9/test/ /data/test/
```
3. **Power off the old ReadyNAS** (192.168.0.9)
4. **Assign static IP to new VM:**
```bash
# On the new VM:
# Edit /etc/network/interfaces to use static config:
cat > /etc/network/interfaces << 'EOF'
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet static
address 192.168.0.9
netmask 255.255.255.0
gateway 192.168.0.254
dns-nameservers 192.168.0.27 192.168.0.6 192.168.1.254
EOF
# Replace "eth0" with actual interface name shown by: ip link show
systemctl restart networking
```
5. **Verify IP assignment:**
```bash
ip addr show
ping -c 3 192.168.0.254
```
6. **Re-enable AD2 sync script** (re-enable scheduled task)
7. **Test from AD2:**
```powershell
$env:RSYNC_PASSWORD = "IQ203s32119"
rsync --list-only rsync://rsync@192.168.0.9/test/
```
8. **Test from Windows:**
```cmd
net use T: \\D2TESTNAS\test
dir T:\
```
9. **Test from DOS machine:** Boot one test station and verify T: drive maps
and NWTOC.BAT runs successfully.
10. **Create baseline snapshot:**
```bash
btrfs-snapshot.sh create daily
```
### Post-Cutover
- [ ] Monitor /var/log/samba/ for connection issues
- [ ] Monitor /var/log/rsyncd.log for sync activity
- [ ] Verify AD2 sync runs successfully (check sync status file)
- [ ] Verify BTRFS snapshot cron is creating snapshots (check after 1 hour)
- [ ] Update credentials.md with any changes to the NAS entry
- [ ] Keep old ReadyNAS powered off but available for 2 weeks as fallback
### Rollback Plan
If critical issues arise during cutover:
1. Power off the new VM
2. Power on the old ReadyNAS
3. Wait 2 minutes for ReadyNAS to boot and claim 192.168.0.9
4. Verify DOS machines can map T: drive
5. Re-enable AD2 sync task
---
## 7. Maintenance Reference
### Service Management
```bash
systemctl status smbd nmbd rsync ssh
systemctl restart smbd # Restart Samba file server
systemctl restart nmbd # Restart NetBIOS name service
systemctl restart rsync # Restart rsync daemon
```
### Snapshot Management
```bash
btrfs-snapshot.sh list # Show all snapshots
btrfs-snapshot.sh create hourly # Manual hourly snapshot
btrfs-snapshot.sh create daily # Manual daily snapshot
btrfs-snapshot.sh prune # Clean up old snapshots per retention policy
```
Snapshots are browsable at `/data/.snapshots/` and via the `\\D2TESTNAS\snapshots`
SMB share (read-only). To restore a file from a snapshot:
```bash
# Find the file in a snapshot:
ls /data/.snapshots/daily_2026-03-12_00-01-00/TS-01/LOGS/
# Copy it back:
cp /data/.snapshots/daily_2026-03-12_00-01-00/TS-01/LOGS/5BLOG/DATA.DAT /data/test/TS-01/LOGS/5BLOG/
```
### Log Files
| Log | Purpose |
|-----|---------|
| /var/log/samba/log.* | Samba per-client logs |
| /var/log/rsyncd.log | rsync daemon transfers |
| /var/log/btrfs-snapshots.log | Snapshot create/prune activity |
| /var/log/auth.log | SSH login attempts |
### Samba User Management
```bash
# Add a new station user with null password:
useradd --system --no-create-home --shell /usr/sbin/nologin ts-51
smbpasswd -a -n ts-51
smbpasswd -e ts-51
# Change engineer password:
smbpasswd engineer
```
---
## 8. Architecture Comparison
| Feature | Old (ReadyNAS RN10400) | New (Debian 13 VM) |
|---------|----------------------|-------------------|
| OS | Netgear ReadyNAS Linux | Debian 13 (Trixie) |
| Filesystem | BTRFS | BTRFS |
| SMB | SMB1 (CORE) | SMB1 (CORE) via Samba |
| rsync | rsync daemon, port 873 | rsync daemon, port 873 |
| Snapshots | 80+ BTRFS (not browsable) | Automated, browsable via SMB |
| NetBIOS/WINS | nmbd | nmbd |
| Management | Web UI (limited) | SSH + CLI (full control) |
| Backup | BTRFS snapshots only | BTRFS snapshots + Hyper-V checkpoints |
| Hardware | Physical appliance | Virtual machine (portable, resizable) |

View File

@@ -0,0 +1,922 @@
#!/bin/bash
# =============================================================================
# setup-d2testnas.sh
# Post-install setup script for Debian 13 (Trixie) VM replacing D2TESTNAS
#
# Purpose: Configure a Debian VM as an SMB1-capable NAS for Dataforth DOS 6.22
# test infrastructure. Replaces Netgear ReadyNAS RN10400 appliance.
#
# Requirements:
# - Fresh Debian 13 (Trixie) minimal install
# - BTRFS partition mounted at /data (or available block device)
# - Root access
# - Network connectivity for package installation
#
# Usage:
# chmod +x setup-d2testnas.sh
# sudo ./setup-d2testnas.sh
#
# Author: Infrastructure automation for Dataforth DOS project
# Date: 2026-03-12
# =============================================================================
set -euo pipefail
# =============================================================================
# Configuration - Edit these values as needed
# =============================================================================
# Network - The VM will initially use DHCP. Change STATIC_IP to the final
# address (192.168.0.9) only during cutover when the old NAS is powered off.
STATIC_IP="192.168.0.9"
NETMASK="255.255.255.0"
GATEWAY="192.168.0.254"
DNS_SERVERS="192.168.0.27 192.168.0.6 192.168.1.254"
HOSTNAME="D2TESTNAS"
WORKGROUP="INTRANET"
# Samba
SMB_SHARE_PATH="/data/test"
SMB_DATASHEETS_PATH="/data/datasheets"
SMB_ENGINEER_USER="engineer"
SMB_ENGINEER_PASS="Engineer1!"
# rsync daemon
RSYNC_MODULE="test"
RSYNC_PATH="/data/test"
RSYNC_USER="rsync"
RSYNC_PASSWORD="IQ203s32119"
# BTRFS snapshot retention
SNAP_HOURLY_RETAIN=48
SNAP_DAILY_RETAIN=30
SNAP_WEEKLY_RETAIN=12
# SSH root password (set during Debian install, but ensure it is correct)
ROOT_PASSWORD="Paper123!@#-nas"
# =============================================================================
# Preflight Checks
# =============================================================================
echo "============================================================"
echo " D2TESTNAS Setup Script - Debian 13 (Trixie)"
echo " Replacing Netgear ReadyNAS for DOS 6.22 infrastructure"
echo "============================================================"
echo ""
if [[ $EUID -ne 0 ]]; then
echo "[ERROR] This script must be run as root."
exit 1
fi
# Check Debian version
if [[ -f /etc/os-release ]]; then
. /etc/os-release
echo "[INFO] Detected OS: ${PRETTY_NAME:-unknown}"
else
echo "[WARNING] Cannot determine OS version. Proceeding anyway."
fi
echo ""
echo "[INFO] This script will:"
echo " 1. Install required packages (samba, rsync, btrfs-progs, etc.)"
echo " 2. Set hostname to ${HOSTNAME}"
echo " 3. Create data directories on /data (BTRFS)"
echo " 4. Configure Samba with SMB1 support for DOS machines"
echo " 5. Configure rsync daemon on port 873"
echo " 6. Set up BTRFS automated snapshots"
echo " 7. Configure and enable all services"
echo ""
echo "Press Ctrl+C within 5 seconds to abort..."
sleep 5
echo ""
# =============================================================================
# Step 1: Package Installation
# =============================================================================
echo "[INFO] Step 1: Installing required packages..."
apt-get update -qq
# Core packages:
# samba - SMB file server (includes nmbd for NetBIOS)
# rsync - rsync daemon
# btrfs-progs - BTRFS filesystem utilities and snapshot management
# openssh-server - SSH access
# cron - scheduled tasks for snapshots
# net-tools - ifconfig, netstat (useful for debugging)
# dnsutils - nslookup, dig (debugging)
DEBIAN_FRONTEND=noninteractive apt-get install -y \
samba \
samba-common \
rsync \
btrfs-progs \
openssh-server \
cron \
net-tools \
dnsutils
echo "[OK] Packages installed successfully."
echo ""
# =============================================================================
# Step 2: Set Hostname
# =============================================================================
echo "[INFO] Step 2: Setting hostname to ${HOSTNAME}..."
hostnamectl set-hostname "${HOSTNAME}"
# Update /etc/hosts so hostname resolves locally
if ! grep -q "${HOSTNAME}" /etc/hosts; then
sed -i "s/^127\.0\.1\.1.*/127.0.1.1\t${HOSTNAME}/" /etc/hosts
# If no 127.0.1.1 line existed, add one
if ! grep -q "127.0.1.1" /etc/hosts; then
echo "127.0.1.1 ${HOSTNAME}" >> /etc/hosts
fi
fi
echo "[OK] Hostname set to ${HOSTNAME}."
echo ""
# =============================================================================
# Step 3: Prepare BTRFS Data Directories
# =============================================================================
echo "[INFO] Step 3: Preparing BTRFS data directories..."
# Check if /data is a BTRFS mount
if mountpoint -q /data 2>/dev/null; then
FS_TYPE=$(df -T /data | tail -1 | awk '{print $2}')
if [[ "${FS_TYPE}" == "btrfs" ]]; then
echo "[OK] /data is mounted as BTRFS."
else
echo "[WARNING] /data is mounted but filesystem is '${FS_TYPE}', not BTRFS."
echo " Snapshots will NOT work. Consider reformatting with BTRFS."
fi
else
echo "[WARNING] /data is not currently mounted."
echo " Checking for available block devices..."
lsblk -f
echo ""
echo " You must mount a BTRFS partition at /data before this script"
echo " can configure snapshots. The script will create directories"
echo " but snapshot functionality requires BTRFS."
echo ""
echo " To create a BTRFS filesystem on /dev/sdX:"
echo " mkfs.btrfs -L d2testnas-data /dev/sdX"
echo " echo '/dev/sdX /data btrfs defaults,noatime,compress=zstd 0 2' >> /etc/fstab"
echo " mkdir -p /data && mount /data"
echo ""
mkdir -p /data
fi
# Create BTRFS subvolumes if on BTRFS, otherwise plain directories
if btrfs subvolume show /data >/dev/null 2>&1 || btrfs filesystem show /data >/dev/null 2>&1; then
BTRFS_AVAILABLE=true
echo "[INFO] BTRFS detected. Creating subvolumes..."
# Create subvolumes for data areas (enables independent snapshots)
if ! btrfs subvolume show /data/test >/dev/null 2>&1; then
btrfs subvolume create /data/test
echo "[OK] Created BTRFS subvolume: /data/test"
else
echo "[INFO] Subvolume /data/test already exists."
fi
if ! btrfs subvolume show /data/datasheets >/dev/null 2>&1; then
btrfs subvolume create /data/datasheets
echo "[OK] Created BTRFS subvolume: /data/datasheets"
else
echo "[INFO] Subvolume /data/datasheets already exists."
fi
# Create snapshot directory
mkdir -p /data/.snapshots
echo "[OK] Snapshot directory created at /data/.snapshots"
else
BTRFS_AVAILABLE=false
echo "[WARNING] BTRFS not available on /data. Creating plain directories."
echo " Snapshot functionality will be disabled."
mkdir -p /data/test
mkdir -p /data/datasheets
fi
# Set permissions - wide open for guest access from DOS machines
chmod 777 /data/test
chmod 777 /data/datasheets
echo "[OK] Data directories ready."
echo ""
# =============================================================================
# Step 4: Configure Samba (SMB1 for DOS 6.22)
# =============================================================================
echo "[INFO] Step 4: Configuring Samba with SMB1 support..."
# Back up original config
if [[ -f /etc/samba/smb.conf ]]; then
cp /etc/samba/smb.conf /etc/samba/smb.conf.orig
fi
cat > /etc/samba/smb.conf << 'SMBCONF'
# =============================================================================
# Samba Configuration for D2TESTNAS
# Replacing Netgear ReadyNAS for Dataforth DOS 6.22 test infrastructure
#
# CRITICAL: DOS 6.22 machines require SMB1 (CORE/COREPLUS/LANMAN1/NT1)
# Modern SMB2/SMB3 is NOT compatible with MS-DOS networking stack.
#
# Shares:
# \\D2TESTNAS\test -> /data/test (T: drive on DOS machines)
# \\D2TESTNAS\datasheets -> /data/datasheets (X: drive on DOS machines)
# \\D2TESTNAS\snapshots -> /data/.snapshots (read-only, browse snapshots)
# =============================================================================
[global]
# --- Identity ---
workgroup = INTRANET
netbios name = D2TESTNAS
server string = D2TESTNAS File Server (DOS Infrastructure)
# --- Protocol: SMB1 Required for DOS 6.22 ---
# CORE is the oldest SMB dialect. DOS MS Client 3.0 uses CORE/COREPLUS.
# NT1 = SMB1 final revision. We allow the full SMB1 range.
server min protocol = CORE
server max protocol = NT1
client min protocol = CORE
client max protocol = NT1
# --- Authentication ---
# DOS machines authenticate with null passwords or guest.
# Security mode must allow this.
security = user
map to guest = Bad User
guest account = nobody
null passwords = yes
# --- NetBIOS / WINS ---
# DOS machines rely on NetBIOS name resolution (no DNS).
# This server acts as a WINS server for the 192.168.0.0/24 network.
wins support = yes
name resolve order = wins lmhosts host bcast
dns proxy = no
local master = yes
preferred master = yes
os level = 65
# --- File Handling for DOS Compatibility ---
# DOS filenames: 8.3 format, case insensitive, no special chars
mangled names = no
case sensitive = no
default case = upper
preserve case = no
short preserve case = no
dos charset = CP437
unix charset = UTF-8
# --- Logging ---
log file = /var/log/samba/log.%m
max log size = 1000
log level = 1
# --- Performance ---
socket options = TCP_NODELAY IPTOS_LOWDELAY
read raw = yes
write raw = yes
# --- Disable features DOS cannot use ---
unix extensions = no
wide links = yes
follow symlinks = yes
# --- Disable printer sharing ---
load printers = no
printing = bsd
printcap name = /dev/null
disable spoolss = yes
# =============================================================================
# Share: test (T: drive for DOS machines)
# =============================================================================
[test]
comment = Dataforth Test Data
path = /data/test
browseable = yes
writable = yes
guest ok = yes
guest only = yes
create mask = 0666
directory mask = 0777
force create mode = 0666
force directory mode = 0777
# DOS compatibility - no oplocks (can cause issues with DOS clients)
oplocks = no
level2 oplocks = no
strict locking = yes
# =============================================================================
# Share: datasheets (X: drive for DOS machines)
# =============================================================================
[datasheets]
comment = Dataforth Datasheets
path = /data/datasheets
browseable = yes
writable = yes
guest ok = yes
guest only = yes
create mask = 0666
directory mask = 0777
force create mode = 0666
force directory mode = 0777
oplocks = no
level2 oplocks = no
strict locking = yes
# =============================================================================
# Share: snapshots (read-only access to BTRFS snapshots)
# =============================================================================
[snapshots]
comment = BTRFS Snapshots (Read Only)
path = /data/.snapshots
browseable = yes
writable = no
guest ok = yes
guest only = yes
SMBCONF
echo "[OK] Samba configuration written to /etc/samba/smb.conf"
# Create Samba users matching the old NAS configuration
# DOS machines use station-specific users (ts-1 through ts-50) with null passwords
echo "[INFO] Creating Samba users for DOS stations..."
for i in $(seq 1 50); do
USERNAME="ts-${i}"
# Create system user if it does not exist (no home dir, no login shell)
if ! id "${USERNAME}" >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin "${USERNAME}" 2>/dev/null || true
fi
# Add to Samba with null password
(echo ""; echo "") | smbpasswd -a -s "${USERNAME}" >/dev/null 2>&1 || true
smbpasswd -n "${USERNAME}" >/dev/null 2>&1 || true
smbpasswd -e "${USERNAME}" >/dev/null 2>&1 || true
done
echo "[OK] Created Samba users ts-1 through ts-50 (null passwords)."
# Create engineer user with password
if ! id "${SMB_ENGINEER_USER}" >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin "${SMB_ENGINEER_USER}" 2>/dev/null || true
fi
(echo "${SMB_ENGINEER_PASS}"; echo "${SMB_ENGINEER_PASS}") | smbpasswd -a -s "${SMB_ENGINEER_USER}" >/dev/null 2>&1 || true
smbpasswd -e "${SMB_ENGINEER_USER}" >/dev/null 2>&1 || true
echo "[OK] Created Samba user 'engineer' with password."
# Validate Samba configuration
echo "[INFO] Validating Samba configuration..."
testparm -s /etc/samba/smb.conf > /dev/null 2>&1 && echo "[OK] Samba config validation passed." || echo "[WARNING] Samba config validation had warnings (check with: testparm)"
echo ""
# =============================================================================
# Step 5: Configure rsync Daemon
# =============================================================================
echo "[INFO] Step 5: Configuring rsync daemon..."
cat > /etc/rsyncd.conf << RSYNCDCONF
# =============================================================================
# rsyncd.conf - rsync daemon configuration for D2TESTNAS
#
# Module "test" provides read/write access to /data/test for the AD2 sync
# script (Sync-FromNAS-rsync.ps1) running on the AD2 Windows server.
#
# The AD2 sync script runs every 15 minutes and does bidirectional sync:
# PULL: NAS -> AD2 (test results: DAT files, TXT reports)
# PUSH: AD2 -> NAS (software updates: ProdSW, UPDATE.BAT, TODO.BAT)
# =============================================================================
uid = root
gid = root
use chroot = true
max connections = 10
timeout = 300
read only = false
# Logging
log file = /var/log/rsyncd.log
transfer logging = yes
log format = %t %a %m %f %l
[${RSYNC_MODULE}]
path = ${RSYNC_PATH}
comment = Dataforth Test Data
read only = false
use chroot = true
auth users = ${RSYNC_USER}
secrets file = /etc/rsyncd.secrets
hosts allow = 192.168.0.0/24 172.16.0.0/12
RSYNCDCONF
echo "[OK] rsync daemon configuration written to /etc/rsyncd.conf"
# Create secrets file
echo "${RSYNC_USER}:${RSYNC_PASSWORD}" > /etc/rsyncd.secrets
chmod 600 /etc/rsyncd.secrets
echo "[OK] rsync secrets file written to /etc/rsyncd.secrets (mode 600)."
# Enable rsync daemon in defaults
# Debian uses /etc/default/rsync to control daemon mode
if [[ -f /etc/default/rsync ]]; then
sed -i 's/^RSYNC_ENABLE=.*/RSYNC_ENABLE=true/' /etc/default/rsync
else
echo "RSYNC_ENABLE=true" > /etc/default/rsync
fi
# Create systemd override to ensure rsync runs as daemon
mkdir -p /etc/systemd/system/rsync.service.d
cat > /etc/systemd/system/rsync.service.d/override.conf << 'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/rsync --daemon --no-detach
EOF
echo "[OK] rsync daemon configured to start on boot."
echo ""
# =============================================================================
# Step 6: Configure SSH (root login with password)
# =============================================================================
echo "[INFO] Step 6: Configuring SSH..."
# Enable root login with password (required for remote management)
SSHD_CONFIG="/etc/ssh/sshd_config"
# Ensure PermitRootLogin is set to yes
if grep -q "^PermitRootLogin" "${SSHD_CONFIG}"; then
sed -i 's/^PermitRootLogin.*/PermitRootLogin yes/' "${SSHD_CONFIG}"
elif grep -q "^#PermitRootLogin" "${SSHD_CONFIG}"; then
sed -i 's/^#PermitRootLogin.*/PermitRootLogin yes/' "${SSHD_CONFIG}"
else
echo "PermitRootLogin yes" >> "${SSHD_CONFIG}"
fi
# Ensure password authentication is enabled
if grep -q "^PasswordAuthentication" "${SSHD_CONFIG}"; then
sed -i 's/^PasswordAuthentication.*/PasswordAuthentication yes/' "${SSHD_CONFIG}"
elif grep -q "^#PasswordAuthentication" "${SSHD_CONFIG}"; then
sed -i 's/^#PasswordAuthentication.*/PasswordAuthentication yes/' "${SSHD_CONFIG}"
else
echo "PasswordAuthentication yes" >> "${SSHD_CONFIG}"
fi
# Debian 13 may use sshd_config.d drop-in files that override main config.
# Disable any drop-in that forces PasswordAuthentication no.
if [[ -d /etc/ssh/sshd_config.d ]]; then
for f in /etc/ssh/sshd_config.d/*.conf; do
if [[ -f "$f" ]] && grep -q "PasswordAuthentication no" "$f" 2>/dev/null; then
sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' "$f"
echo "[INFO] Updated ${f} to allow password authentication."
fi
done
fi
# Set root password
echo "root:${ROOT_PASSWORD}" | chpasswd
echo "[OK] Root password set."
echo "[OK] SSH configured: root login enabled, password auth enabled."
echo ""
# =============================================================================
# Step 7: BTRFS Snapshot Automation
# =============================================================================
echo "[INFO] Step 7: Setting up BTRFS snapshot automation..."
# Create the snapshot management script
cat > /usr/local/bin/btrfs-snapshot.sh << 'SNAPSCRIPT'
#!/bin/bash
# =============================================================================
# btrfs-snapshot.sh - Automated BTRFS snapshot management for D2TESTNAS
#
# Usage:
# btrfs-snapshot.sh create <hourly|daily|weekly>
# btrfs-snapshot.sh prune
# btrfs-snapshot.sh list
#
# Snapshots are stored in /data/.snapshots/ with naming convention:
# /data/.snapshots/<type>_YYYY-MM-DD_HH-MM-SS
#
# Retention policy:
# Hourly: 48 snapshots (2 days)
# Daily: 30 snapshots (1 month)
# Weekly: 12 snapshots (3 months)
# =============================================================================
set -euo pipefail
SNAP_DIR="/data/.snapshots"
SUBVOLUME="/data/test"
HOURLY_RETAIN=48
DAILY_RETAIN=30
WEEKLY_RETAIN=12
create_snapshot() {
local snap_type="${1}"
local timestamp
timestamp=$(date '+%Y-%m-%d_%H-%M-%S')
local snap_name="${snap_type}_${timestamp}"
local snap_path="${SNAP_DIR}/${snap_name}"
if [[ ! -d "${SNAP_DIR}" ]]; then
mkdir -p "${SNAP_DIR}"
fi
# Verify source is a BTRFS subvolume
if ! btrfs subvolume show "${SUBVOLUME}" >/dev/null 2>&1; then
echo "[ERROR] ${SUBVOLUME} is not a BTRFS subvolume. Cannot create snapshot."
exit 1
fi
btrfs subvolume snapshot -r "${SUBVOLUME}" "${snap_path}"
echo "[OK] Created snapshot: ${snap_name}"
}
prune_snapshots() {
local snap_type="${1}"
local retain="${2}"
# List snapshots of this type, sorted oldest first
local snaps
snaps=$(find "${SNAP_DIR}" -maxdepth 1 -name "${snap_type}_*" -type d | sort)
local count
count=$(echo "${snaps}" | grep -c . 2>/dev/null || echo 0)
if [[ ${count} -le ${retain} ]]; then
return
fi
local to_delete=$((count - retain))
echo "[INFO] Pruning ${to_delete} old ${snap_type} snapshot(s) (keeping ${retain})..."
echo "${snaps}" | head -n "${to_delete}" | while read -r snap; do
if [[ -n "${snap}" && -d "${snap}" ]]; then
btrfs subvolume delete "${snap}"
echo "[OK] Deleted: $(basename "${snap}")"
fi
done
}
list_snapshots() {
echo "=== BTRFS Snapshots in ${SNAP_DIR} ==="
echo ""
if [[ ! -d "${SNAP_DIR}" ]]; then
echo "No snapshot directory found."
return
fi
for snap_type in hourly daily weekly; do
local count
count=$(find "${SNAP_DIR}" -maxdepth 1 -name "${snap_type}_*" -type d 2>/dev/null | wc -l)
echo "${snap_type}: ${count} snapshot(s)"
find "${SNAP_DIR}" -maxdepth 1 -name "${snap_type}_*" -type d 2>/dev/null | sort | while read -r s; do
echo " $(basename "${s}")"
done
echo ""
done
}
case "${1:-}" in
create)
snap_type="${2:-hourly}"
create_snapshot "${snap_type}"
;;
prune)
prune_snapshots "hourly" "${HOURLY_RETAIN}"
prune_snapshots "daily" "${DAILY_RETAIN}"
prune_snapshots "weekly" "${WEEKLY_RETAIN}"
;;
list)
list_snapshots
;;
*)
echo "Usage: $0 {create <hourly|daily|weekly>|prune|list}"
exit 1
;;
esac
SNAPSCRIPT
chmod +x /usr/local/bin/btrfs-snapshot.sh
# Install cron jobs for snapshot automation
CRON_FILE="/etc/cron.d/btrfs-snapshots"
cat > "${CRON_FILE}" << 'CRONEOF'
# BTRFS Snapshot Automation for D2TESTNAS
# Hourly snapshots: every hour on the hour
# Daily snapshots: midnight
# Weekly snapshots: Sunday at 00:15
# Pruning: runs after each snapshot creation
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# Hourly: create snapshot, then prune old ones
0 * * * * root /usr/local/bin/btrfs-snapshot.sh create hourly >> /var/log/btrfs-snapshots.log 2>&1 && /usr/local/bin/btrfs-snapshot.sh prune >> /var/log/btrfs-snapshots.log 2>&1
# Daily: create at midnight (separate from hourly for distinct retention)
1 0 * * * root /usr/local/bin/btrfs-snapshot.sh create daily >> /var/log/btrfs-snapshots.log 2>&1 && /usr/local/bin/btrfs-snapshot.sh prune >> /var/log/btrfs-snapshots.log 2>&1
# Weekly: create on Sunday at 00:15
15 0 * * 0 root /usr/local/bin/btrfs-snapshot.sh create weekly >> /var/log/btrfs-snapshots.log 2>&1 && /usr/local/bin/btrfs-snapshot.sh prune >> /var/log/btrfs-snapshots.log 2>&1
CRONEOF
chmod 644 "${CRON_FILE}"
# Set up log rotation for snapshot log
cat > /etc/logrotate.d/btrfs-snapshots << 'LOGROTEOF'
/var/log/btrfs-snapshots.log {
weekly
rotate 8
compress
missingok
notifempty
}
LOGROTEOF
if [[ "${BTRFS_AVAILABLE}" == "true" ]]; then
echo "[OK] BTRFS snapshot automation configured."
echo " Hourly: retained ${SNAP_HOURLY_RETAIN} (2 days)"
echo " Daily: retained ${SNAP_DAILY_RETAIN} (1 month)"
echo " Weekly: retained ${SNAP_WEEKLY_RETAIN} (3 months)"
echo " Snapshots browsable via: \\\\D2TESTNAS\\snapshots"
else
echo "[WARNING] BTRFS not available. Snapshot cron jobs installed but will"
echo " fail until /data is a BTRFS filesystem with subvolumes."
fi
echo ""
# =============================================================================
# Step 8: Network Configuration (prepare for cutover)
# =============================================================================
echo "[INFO] Step 8: Preparing network configuration..."
# Identify the primary network interface
PRIMARY_IFACE=$(ip route show default 2>/dev/null | awk '{print $5}' | head -1)
if [[ -z "${PRIMARY_IFACE}" ]]; then
PRIMARY_IFACE="eth0"
echo "[WARNING] Could not detect primary interface. Defaulting to ${PRIMARY_IFACE}."
else
echo "[INFO] Detected primary network interface: ${PRIMARY_IFACE}"
fi
# Write a static IP configuration file (NOT activated yet)
# This file is for cutover day when we switch from old NAS to new VM.
STATIC_CONFIG="/etc/network/interfaces.d/static-cutover"
cat > "${STATIC_CONFIG}" << NETEOF
# =============================================================================
# CUTOVER STATIC IP CONFIGURATION
# =============================================================================
# This file is NOT active by default. The VM uses DHCP during testing.
#
# To activate for cutover:
# 1. Power off the old D2TESTNAS (ReadyNAS)
# 2. Edit /etc/network/interfaces:
# - Comment out or remove the DHCP line for ${PRIMARY_IFACE}
# - Add: source /etc/network/interfaces.d/static-cutover
# 3. Run: systemctl restart networking
# 4. Verify: ip addr show ${PRIMARY_IFACE}
#
# Alternatively, replace /etc/network/interfaces entirely with:
# auto lo
# iface lo inet loopback
# auto ${PRIMARY_IFACE}
# iface ${PRIMARY_IFACE} inet static
# address ${STATIC_IP}
# netmask ${NETMASK}
# gateway ${GATEWAY}
# dns-nameservers ${DNS_SERVERS}
# =============================================================================
auto ${PRIMARY_IFACE}
iface ${PRIMARY_IFACE} inet static
address ${STATIC_IP}
netmask ${NETMASK}
gateway ${GATEWAY}
dns-nameservers ${DNS_SERVERS}
NETEOF
echo "[OK] Static IP config prepared at ${STATIC_CONFIG}"
echo " Current IP: $(ip -4 addr show "${PRIMARY_IFACE}" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1 || echo 'unknown')"
echo " Cutover IP: ${STATIC_IP} (activate during cutover)"
echo ""
# =============================================================================
# Step 9: Enable and Start Services
# =============================================================================
echo "[INFO] Step 9: Enabling and starting services..."
systemctl daemon-reload
# Samba (smbd for file sharing, nmbd for NetBIOS name resolution)
systemctl enable smbd
systemctl enable nmbd
systemctl restart smbd
systemctl restart nmbd
echo "[OK] Samba (smbd + nmbd) enabled and started."
# rsync daemon
systemctl enable rsync
systemctl restart rsync
echo "[OK] rsync daemon enabled and started."
# SSH
systemctl enable ssh
systemctl restart ssh
echo "[OK] SSH enabled and started."
# cron (should already be running, ensure it is)
systemctl enable cron
systemctl restart cron
echo "[OK] cron enabled and started."
echo ""
# =============================================================================
# Step 10: Firewall (if ufw/nftables is active)
# =============================================================================
echo "[INFO] Step 10: Checking firewall..."
if command -v ufw >/dev/null 2>&1 && ufw status | grep -q "active"; then
echo "[INFO] UFW is active. Adding required rules..."
ufw allow 22/tcp comment "SSH"
ufw allow 137/udp comment "NetBIOS Name Service (nmbd)"
ufw allow 138/udp comment "NetBIOS Datagram"
ufw allow 139/tcp comment "NetBIOS Session (SMB1)"
ufw allow 445/tcp comment "SMB"
ufw allow 873/tcp comment "rsync daemon"
echo "[OK] Firewall rules added."
elif command -v nft >/dev/null 2>&1; then
echo "[INFO] nftables available. If you enable a firewall later, allow these ports:"
echo " 22/tcp (SSH), 137/udp (NetBIOS), 138/udp (NetBIOS),"
echo " 139/tcp (SMB1), 445/tcp (SMB), 873/tcp (rsync)"
else
echo "[INFO] No active firewall detected. No rules needed."
fi
echo ""
# =============================================================================
# Step 11: Verification
# =============================================================================
echo "[INFO] Step 11: Running verification checks..."
echo ""
ERRORS=0
# Check smbd
if systemctl is-active --quiet smbd; then
echo "[OK] smbd is running."
else
echo "[ERROR] smbd is NOT running."
ERRORS=$((ERRORS + 1))
fi
# Check nmbd
if systemctl is-active --quiet nmbd; then
echo "[OK] nmbd is running."
else
echo "[ERROR] nmbd is NOT running."
ERRORS=$((ERRORS + 1))
fi
# Check rsync
if systemctl is-active --quiet rsync; then
echo "[OK] rsync daemon is running."
else
echo "[ERROR] rsync daemon is NOT running."
ERRORS=$((ERRORS + 1))
fi
# Check rsync port
if ss -tlnp | grep -q ":873"; then
echo "[OK] rsync daemon listening on port 873."
else
echo "[ERROR] rsync daemon NOT listening on port 873."
ERRORS=$((ERRORS + 1))
fi
# Check SSH
if systemctl is-active --quiet ssh; then
echo "[OK] SSH is running."
else
echo "[ERROR] SSH is NOT running."
ERRORS=$((ERRORS + 1))
fi
# Check Samba shares
echo ""
echo "[INFO] Samba shares configured:"
smbclient -L localhost -N 2>/dev/null | grep -E "Disk|test|datasheets|snapshots" || echo "[WARNING] Could not list Samba shares (smbclient may not support -N with this config)."
# Check data directories
echo ""
if [[ -d /data/test ]]; then
echo "[OK] /data/test exists."
else
echo "[ERROR] /data/test does NOT exist."
ERRORS=$((ERRORS + 1))
fi
if [[ -d /data/datasheets ]]; then
echo "[OK] /data/datasheets exists."
else
echo "[ERROR] /data/datasheets does NOT exist."
ERRORS=$((ERRORS + 1))
fi
echo ""
# =============================================================================
# Summary
# =============================================================================
CURRENT_IP=$(ip -4 addr show "${PRIMARY_IFACE}" 2>/dev/null | grep -oP 'inet \K[\d.]+' | head -1 || echo 'unknown')
echo "============================================================"
echo " SETUP COMPLETE"
echo "============================================================"
echo ""
echo " Hostname: ${HOSTNAME}"
echo " Current IP: ${CURRENT_IP} (DHCP - for testing)"
echo " Cutover IP: ${STATIC_IP} (activate during cutover)"
echo " Interface: ${PRIMARY_IFACE}"
echo ""
echo " Services:"
echo " Samba (smbd): $(systemctl is-active smbd)"
echo " NetBIOS (nmbd): $(systemctl is-active nmbd)"
echo " rsync daemon: $(systemctl is-active rsync)"
echo " SSH: $(systemctl is-active ssh)"
echo " cron: $(systemctl is-active cron)"
echo ""
echo " Shares:"
echo " \\\\${HOSTNAME}\\test -> /data/test"
echo " \\\\${HOSTNAME}\\datasheets -> /data/datasheets"
echo " \\\\${HOSTNAME}\\snapshots -> /data/.snapshots (read-only)"
echo ""
echo " rsync:"
echo " Module: ${RSYNC_MODULE} -> ${RSYNC_PATH}"
echo " Auth: ${RSYNC_USER} (port 873)"
echo ""
echo " BTRFS Snapshots: $(if [[ ${BTRFS_AVAILABLE} == true ]]; then echo 'ACTIVE'; else echo 'DISABLED (no BTRFS)'; fi)"
echo " Hourly: ${SNAP_HOURLY_RETAIN} retained | Daily: ${SNAP_DAILY_RETAIN} retained | Weekly: ${SNAP_WEEKLY_RETAIN} retained"
echo " List: btrfs-snapshot.sh list"
echo " Manual: btrfs-snapshot.sh create <hourly|daily|weekly>"
echo ""
if [[ ${ERRORS} -gt 0 ]]; then
echo " [WARNING] ${ERRORS} verification check(s) failed. Review output above."
else
echo " [OK] All verification checks passed."
fi
echo ""
echo "============================================================"
echo " CUTOVER CHECKLIST"
echo "============================================================"
echo ""
echo " When ready to replace the old ReadyNAS:"
echo ""
echo " 1. Copy data from old NAS:"
echo " rsync -avz rsync://rsync@192.168.0.9/test/ /data/test/"
echo ""
echo " 2. Stop the old NAS (power off D2TESTNAS ReadyNAS)"
echo ""
echo " 3. Switch this VM to static IP ${STATIC_IP}:"
echo " Edit /etc/network/interfaces to source ${STATIC_CONFIG}"
echo " systemctl restart networking"
echo ""
echo " 4. Verify from a Windows machine:"
echo " net use T: \\\\${STATIC_IP}\\test"
echo " rsync --list-only rsync://rsync@${STATIC_IP}/test/"
echo ""
echo " 5. Verify from a DOS machine:"
echo " Boot a test station and confirm T: drive maps successfully"
echo ""
echo " 6. Create initial BTRFS snapshot:"
echo " btrfs-snapshot.sh create daily"
echo ""
echo "============================================================"

View File

@@ -0,0 +1,61 @@
# TestDataDB Investigation - Missing Recent Records
**Date:** 2026-03-11
**Status:** BLOCKED - VPN down, need parser source code from AD2
## Problem
The test database on AD2 has newest `test_date` of 2026-01-19 despite daily tests being run every weekday. After a full re-import (1,028,275 -> 1,632,793 records), the max date did not change.
## Key Evidence
1. DAT files with TODAY's timestamps (2026-03-11 13:29-13:30) exist on AD2 at `C:\Shares\test\TS-1L\LOGS\5BLOG\`
2. These files were processed by the full import (604,518 new records added)
3. But `MAX(test_date)` is still 2026-01-19
## Sample DAT File Content (33-06D.DAT from 5BLOG)
```
"SCM5B33-06D "
9.528068E-02,.9528068,.9696456,.1683873,"PASS"
.3142081,3.142081,3.155015,.1293349,"PASS"
.5374944,5.374944,5.380653,5.708694E-02,"PASS"
.7651215,7.651215,7.651233,1.811981E-04,"PASS"
.997809,9.97809,9.967015,-.1107502,"PASS"
"PASS 92.941","","PASS .16838733","PASS 2.045663E-023","PASS-2.213427E-023"
"","PASS .29938753","","PASS .79032473","PASS .05982453"
"","PASS 117.35150","","PASS-.1008325","PASS 91.444891"
"PASS 1.024068E-023",""
```
**No date anywhere in the content or filename.**
## Root Cause Hypothesis
The `multiline.js` parser at `C:\Shares\testdatadb\parsers\multiline.js` determines `test_date` from some source. Possibilities:
1. **File modification time (mtime)** - most likely since there's no date in content
2. **A date field elsewhere in larger files** - maybe we only saw a partial file
3. **A hardcoded date or fallback** - parser might have a bug
If parser uses mtime, the question is whether mtime is preserved when:
- DOS machines XCOPY files to NAS
- Sync-FromNAS.ps1 SCPs files from NAS to AD2
- SCP with -O flag may or may not preserve timestamps
## HISTLOGS vs Station Files
HISTLOGS files at `C:\Shares\test\Ate\HISTLOGS\` are the authoritative consolidated source. These may have a DIFFERENT format than the per-station DAT files. The initial import (1,030,940 records) came mostly from HISTLOGS (576K) and Recovery-TEST (454K), with only 59 from live station data.
The 604K new records from the re-import might all be from HISTLOGS/Recovery with dates up to Jan 19, while the per-station files might be producing 0 records or records with the same old dates.
## Next Steps (when VPN reconnects)
1. **READ THE PARSER:** `ssh sysadmin@AD2 "type C:\Shares\testdatadb\parsers\multiline.js"`
2. **Check a specific record:** Query DB for records from `33-06D.DAT` source file to see what test_date was assigned
3. **Check import logs:** `ssh sysadmin@AD2 "type C:\Shares\test\scripts\sync-from-nas.log"` for any import errors
4. **Verify HISTLOGS content:** Check if HISTLOGS files have different format than station files
## Deployed Fixes This Session
- Sync-FromNAS.ps1: Get-NASFileList fix (stdout deadlock), 8.3 filename filtering, SCP path escaping
- import.js: Changed INSERT OR IGNORE to INSERT OR REPLACE
- Both deployed to AD2 at C:\Shares\test\scripts\ and C:\Shares\testdatadb\database\
- Commit dd4086d (local only, not pushed - Gitea unreachable)
## Session Context
- Worked on from Windows machine (ACG-M-L5090)
- VPN went down during investigation
- Previous session summary in conversation compaction
- User said "continue to work this problem - we need to find those records"

View File

@@ -0,0 +1,366 @@
# Session Log: 2026-03-12 - D2TESTNAS VM Build, NAS Migration, Rsync Sync Fix
## Session Summary
Major infrastructure session: replaced broken SCP-based sync with rsync, built a new Debian 13 VM to replace the aging ReadyNAS, transferred data, and performed IP cutover. Also investigated BTRFS snapshots on old NAS and began DOS machine testing against new Linux-based NAS.
### Key Accomplishments
1. **Fixed Sync-FromNAS.ps1 on AD2** - Replaced broken SCP with rsync daemon protocol, added guards for stray files (TS-21, TS-3R/HVLOG), added log file write retry for AV locking
2. **Disabled old SCP scheduled tasks on AD2** - Killed Sync-FromNAS and BulkSync-Catchup tasks
3. **Built D2TESTNAS replacement VM** on DF-HYPERV-B (Debian 13, Samba SMB1, rsync daemon, BTRFS 512GB data disk)
4. **Transferred data from old NAS** - test/ data (~24GB+), datasheets, home, 82 snapshots (partial ~43GB logical)
5. **IP cutover completed** - New VM now at 192.168.0.9, old NAS on DHCP at 192.168.0.117
6. **WINS/NetBIOS conflict resolved** - Killed nmbd on old NAS, removed auto-restart cron, blocked ports 137/138 via iptables
### Key Decisions
- Chose Hyper-V VM on DF-HYPERV-B over repurposing physical server DF-SVR-D2-SYNC
- Used BTRFS for data disk with subvolumes for test and datasheets
- Single rsync stream to avoid overloading old NAS (ARM processor)
- BTRFS snapshots from old NAS are being flattened (CoW -> full copies) which makes them much larger than ReadyNAS UI reported
### Problems Encountered and Solutions
- **TS-21 stray file**: 1,129-byte DAT file from 2012 existed instead of directory. Renamed, added script guard.
- **TS-3R/LOGS/HVLOG stray file**: 56-byte file from 2013. Same fix.
- **Log file locking**: AV locking sync-from-nas.log. Added 3-retry with 100ms delay.
- **AD2 high latency**: AV causing 685-1056ms ping. Recommended exclusions.
- **NAS freezing under SSH load**: Power cycled, limited to single rsync stream.
- **nmbd auto-restart on old NAS**: Cron `*/5 * * * * pgrep -x nmbd || /usr/sbin/nmbd -D`. Removed cron, blocked ports via iptables.
- **nmcli config didn't save first attempt**: SSH dropped before apply. Re-ran successfully.
- **DOS Error 53 (network path not found)**: Old NAS still broadcasting D2TESTNAS name. Fixed by killing nmbd and blocking NetBIOS ports.
---
## Credentials
### New D2TESTNAS VM (Debian 13)
- **IP**: 192.168.0.9 (static via NetworkManager)
- **SSH**: root / Paper123!@# (also localadmin / Paper123!@#)
- **SSH Key**: ed25519 generated on VM, public key installed on old NAS
- **Key fingerprint**: SHA256:S2Eom4RwHS/8YMu+ePnOmDOJxGhIkxJQ2ocR3WsH24o root@D2TESTNAS
### Rsync Daemon (new VM)
- **Port**: 873
- **Module**: test = /data/test
- **User**: rsync
- **Password**: IQ203s32119
- **Config**: /etc/rsyncd.conf
- **Secrets**: /etc/rsyncd.secrets
### Samba (new VM)
- **Shares**: test (/data/test), datasheets (/data/datasheets), snapshots (/data/test/.snapshots)
- **Protocol**: SMB1 (CORE) through SMB3
- **Auth**: Guest OK on all shares
- **Workgroup**: D2TESTING
- **NetBIOS name**: D2TESTNAS
- **WINS support**: yes
### Old NAS (ReadyNAS)
- **Current IP**: 192.168.0.117 (DHCP, was 192.168.0.9)
- **MAC**: 28:C6:8E:34:4B:5E
- **SSH**: root (key-based auth from new VM)
- **Status**: nmbd killed, cron cleared, NetBIOS ports blocked via iptables. Samba stopped. SSH still works for rsync transfers.
### AD2 (Windows Server)
- **IP**: 192.168.0.6
- **Sync script**: C:\Scripts\Sync-FromNAS-rsync.ps1 (deployed, dry-run validated)
- **Test data path**: C:\Shares\test\
- **cwRsync**: Installed via Chocolatey
### DF-SVR-D2-SYNC (unused physical server)
- **IP**: 192.168.0.93
- **Creds**: sysadmin / Paper123!@#
- **HP ProLiant ML350 G6, 64GB RAM, Server 2019**
- **SMB share**: NAS-BACKUP (was used temporarily for CIFS backup attempt)
- **SMB1 enabled** on this server
### UDM Network
- **WINS server**: 192.168.0.9 (configured in UDM DHCP option 44)
---
## Infrastructure
### New D2TESTNAS VM Configuration
- **Host**: DF-HYPERV-B (dedicated Hyper-V host)
- **OS**: Debian 13 (Trixie)
- **Network**: eth0, static 192.168.0.9/24, gateway 192.168.0.1
- **Disks**:
- /dev/sda: OS disk
- /dev/sdb: 512GB BTRFS data disk mounted at /data
- **BTRFS subvolumes**: test, datasheets (under /data)
- **Services**: smbd, nmbd, rsync (daemon), sshd, cron
- **Snapshot cron**:
```
0 * * * * /usr/local/bin/btrfs-snapshot.sh test 48
0 * * * * /usr/local/bin/btrfs-snapshot.sh datasheets 48
0 0 * * * /usr/local/bin/btrfs-snapshot.sh test 30
0 0 * * 0 /usr/local/bin/btrfs-snapshot.sh test 12
```
### Key Config Files on New VM
- `/etc/samba/smb.conf` - Samba config (SMB1/CORE, DOS charset CP437, WINS)
- `/etc/rsyncd.conf` - rsync daemon (module "test")
- `/etc/rsyncd.secrets` - rsync auth (rsync:IQ203s32119)
- `/usr/local/bin/btrfs-snapshot.sh` - BTRFS snapshot script
### Data Transfer Status (as of ~18:30)
- **test/ data (excl snapshots)**: ~24 GB transferred, rsync still running (single stream from .117)
- **test/ snapshots**: ~43 GB logical transferred (82 snapshots), transfer was stopped to reduce NAS load - needs restart
- **datasheets/ + snapshots**: Complete (2.3 MB + 82 snapshot dirs)
- **home/**: Complete (612 KB)
- **Disk usage**: ~26 GB actual on BTRFS (CoW dedup), 486 GB free
- **Note**: ReadyNAS UI reported 5.26GB data + 16.28GB snapshots, but actual rsync transfer is MUCH larger due to BTRFS CoW flattening
---
## Files Created/Modified
### New Files
- `D:\ClaudeTools\projects\dataforth-dos\sync-fixes\Sync-FromNAS-rsync.ps1` - Complete rsync-based replacement sync script (deployed to AD2)
- `D:\ClaudeTools\projects\dataforth-dos\d2testnas-vm\setup-d2testnas.sh` - 522-line post-install setup script
- `D:\ClaudeTools\projects\dataforth-dos\d2testnas-vm\README.md` - Hyper-V creation commands, Debian install notes, cutover checklist
### Script Fixes Applied (Sync-FromNAS-rsync.ps1)
1. **Directory-only filter** for NAS station enumeration (line ~125)
2. **Station path guard** - detects stray files where directories expected
3. **Log type directory guard** - renames stray files in LOGS subdirs
4. **Write-Log retry** - 3 attempts with 100ms delay for AV file locking
### Deployed to New VM (via SSH)
- /etc/samba/smb.conf (full Samba config)
- /etc/rsyncd.conf + /etc/rsyncd.secrets
- /usr/local/bin/btrfs-snapshot.sh + cron entries
- SSH key pair generated, public key added to old NAS
---
## Pending/Incomplete Tasks
### Immediate (resume next session)
1. **Monitor test/ data rsync** - Single stream running from old NAS (.117) to new VM (.9). Check with:
```bash
ssh root@192.168.0.9 "ps aux | grep 'rsync -av' | grep -v grep; du -sh /data/test/ --exclude=.snapshots"
```
2. **Restart snapshot transfer** after data transfer completes:
```bash
ssh root@192.168.0.9 "nohup bash -c 'rsync -av root@192.168.0.117:/data/test/.snapshots/ /data/test/.snapshots/ 2>&1 | tail -5' &"
```
3. **Test DOS machine connectivity** - Error 53 was resolved (old NAS NetBIOS killed). Need to reboot DOS machine and test:
- `NET USE T: \\D2TESTNAS\TEST`
- Run CTONW.BAT (copy logs to NAS)
- Run NWTOC.BAT (download updates from NAS)
- Verify files appear in /data/test/TS-XX/LOGS/ on new VM
### After Data Transfer Complete
4. **Verify data integrity** - Compare file counts/sizes between old and new NAS
5. **Power off old NAS** once all data confirmed transferred
6. **Set up scheduled task on AD2** - Create 15-minute scheduled task for Sync-FromNAS-rsync.ps1
7. **Run real (non-dry) sync on AD2** - Execute Sync-FromNAS-rsync.ps1 without -DryRun flag
8. **AV exclusions on AD2** - Add exclusions for C:\Shares\test\ and rsync.exe
### Nice to Have
9. **Copy NAS config backup to new VM** (already backed up to DF-SVR-D2-SYNC)
10. **Datto Workplace SmartBadge research** - Researched that SmartBadge add-in for Excel doesn't exist; Workplace integrates via sync client and web, not Excel plugin
---
## DOS Machine Data Flow
```
DOS 6.22 (C:\ATE\) --COPY--> T:\MACHINE\LOGS\ (NAS via SMB1)
|
v (rsync daemon, port 873)
AD2 C:\Shares\test\
|
v (future: database ingestion)
MariaDB @ 172.16.3.30
```
### Batch Files (DOS -> NAS)
- **CTONW.BAT v3.2** - Uses COPY (not XCOPY) to upload log files from C:\ATE\ to T:\MACHINE\LOGS\
- **NWTOC.BAT v3.5** - Uses COPY to download updates from T:\COMMON\ProdSW\ to C:\BAT\ and C:\ATE\
- **UPDATE.BAT v2.1** - Uses XCOPY for full machine backup (had /D flag fix for DOS 6.22)
### Log Types
5BLOG, 7BLOG, 8BLOG, DSCLOG, SCTLOG, VASLOG, PWRLOG, HVLOG
### Active Stations
TS-3L (most recent activity), TS-4R, TS-3R, TS-11L, TS-GURU, plus many others
---
## Reference
### Key Commands
```bash
# SSH to new D2TESTNAS
ssh root@192.168.0.9
# SSH to old NAS (DHCP)
ssh root@192.168.0.117
# Check rsync transfers on new VM
ssh root@192.168.0.9 "ps aux | grep rsync | grep -v grep"
# Test Samba from Windows
net view \\192.168.0.9
smbclient -L //192.168.0.9 -N
# Test rsync daemon
rsync rsync://rsync@192.168.0.9/test/
# Restart services on new VM
ssh root@192.168.0.9 "systemctl restart smbd nmbd rsync"
# BTRFS snapshot status
ssh root@192.168.0.9 "ls /data/test/.snapshots/"
```
### Old NAS Lockdown Commands (already applied)
```bash
# Block NetBIOS (prevents name conflict)
ssh root@192.168.0.117 "iptables -A INPUT -p udp --dport 137 -j DROP; iptables -A INPUT -p udp --dport 138 -j DROP; iptables -A OUTPUT -p udp --sport 137 -j DROP; iptables -A OUTPUT -p udp --sport 138 -j DROP"
# Remove auto-restart cron
ssh root@192.168.0.117 "crontab -r"
```
---
## Session Timeline
- Started: ~14:00 (context recovery from previous session)
- Rsync script fixes and deployment to AD2
- Disabled old SCP scheduled tasks
- Investigated BTRFS snapshots (81 found)
- Built D2TESTNAS VM on DF-HYPERV-B (Debian 13)
- Configured all services (Samba, rsync, BTRFS, SSH)
- Started data transfer from old NAS
- Killed snapshot transfer to reduce NAS load (single stream)
- IP cutover: new VM .185 -> .9, old NAS .9 -> DHCP .117
- Resolved WINS conflict (killed old NAS nmbd, removed cron, blocked ports)
- DOS machine testing started - Error 53 resolved
- Data transfer ongoing (~24GB+ transferred, snapshots pending restart)
- Session saved: ~18:45
## Update: ~19:30 - Batch File Fix and DOS Machine Testing
### DOS Machine Testing Results
- All 4 tested machines (TS-3L, TS-3R, TS-4L, TS-4R) connected to new Linux NAS successfully
- T: drive mapped via NetBIOS name (after killing old NAS nmbd)
- Files successfully copied (3 .LOG files)
- BUT: "Bad command or file name" (5x) and "Too many parameters" (5x) errors from IF EXIST/IF NOT EXIST commands
- Confirmed CTONW.BAT v3.2 on machine, correct line endings (CR+LF verified via DEBUG)
- Root cause: DOS 6.22 IF EXIST command failing on network paths - likely SMB1 compatibility issue with wildcard queries
### Fix Applied: Batch Files v4.0
Eliminated all IF EXIST/IF NOT EXIST checks from startup batch files. Directories pre-created on server.
**CTONW.BAT v4.0** - Direct COPY commands, no IF EXIST guards. Target dirs pre-created on NAS.
**NWTOC.BAT v4.0** - Direct MD and COPY commands, no IF EXIST guards. MD harmless if dir exists locally.
**AUTOEXEC.BAT v4.0** - Removed IF EXIST around CALL commands, direct MD for local dirs.
All deployed to NAS at `/data/test/COMMON/ProdSW/`. Machines will pick up new versions on next boot via NWTOC download.
### Pre-created Directories on NAS
Ran script to create LOGS/5BLOG, LOGS/7BLOG, LOGS/8BLOG, LOGS/DSCLOG, LOGS/HVLOG, LOGS/PWRLOG, LOGS/SCTLOG, LOGS/VASLOG, and Reports for ALL TS-* station directories.
### Old NAS Status
- DHCP at 192.168.0.117
- nmbd killed, cron removed, NetBIOS ports 137/138 blocked via iptables
- rsync data transfer still running (single stream, ~24GB+ transferred)
- Snapshot transfer stopped (was at ~43GB logical), needs restart after data completes
### Pending
1. Reboot a DOS machine to test v4.0 batch files (second boot needed for NWTOC v4.0)
2. Monitor data transfer completion (rsync single stream still running as of ~20:00)
3. Restart snapshot transfer after data completes
4. Verify test data appears in correct LOGS subdirectories on NAS
5. Set up AD2 scheduled task for rsync sync
6. Run real (non-dry) Sync-FromNAS-rsync.ps1 on AD2
## Update: ~20:00 - DEPLOY Trailing Space Bug and Data Upload Success
### Critical Bug Found: DEPLOY.BAT Trailing Space
- **Root cause of ALL "Too many parameters" errors**: `ECHO SET MACHINE=%MACHINE% >> C:\AUTOEXEC.BAT` includes the space before `>>` in the output
- This sets `MACHINE=TS-3L ` (with trailing space) which causes `T:\TS-3L \LOGS\DSCLOG` to be parsed as two parameters
- **Fix**: DEPLOY v4.1 moves redirect before ECHO: `>>C:\AUTOEXEC.BAT ECHO SET MACHINE=%MACHINE%`
- First line uses `>` (overwrite), rest use `>>` (append)
- DEPLOY v4.1 deployed to NAS at `/data/test/COMMON/ProdSW/DEPLOY.BAT`
### Samba Case Sensitivity - Confirmed OK
- `smb.conf` has `case sensitive = no` and `default case = upper`
- No duplicate directories (only `TS-4L` exists, not `ts-4L`)
### TS-3L Deploy Test
- Ran `T:\UPDATE TS-3L` which calls DEPLOY v4.0 (before trailing space fix)
- DEPLOY completed, files confirmed v4.0 on machine via TYPE
- After reboot: NAS still showed old CTONW.LOG/NWTOC.LOG - MACHINE had trailing space
- Running CTONW manually showed 9x "Too many parameters" on all COPY-to-subdirectory lines
- `COPY C:\ATE\*.LOG T:\%MACHINE%` worked (no subdirectory in path) but `COPY ... T:\%MACHINE%\LOGS\DSCLOG` failed
- This confirmed the trailing space theory - space before `\LOGS\` splits the path
### TS-4L Data Upload - SUCCESS
- TS-4L uploaded data at 20:10 with clean MACHINE variable (no trailing space)
- **84 test data files uploaded to NAS:**
- 5BLOG: 20 files
- 7BLOG: 29 files (historical .SHT files)
- 8BLOG: 10 files
- DSCLOG: 21 files (including today's 38-02.DAT from 03-12-26)
- SCTLOG: 2 files
- VASLOG: 2 files
- **90+ work-order Reports** (.TXT files) uploaded to TS-4L/Reports/
- **3 LOG files** (NWTOC.LOG, CTONW.LOG, CTONWTXT.LOG)
- CTONW.LOG confirms: `CTONW.BAT v4.0 / Machine: TS-4L` (no trailing space)
### Original STARTNET.BAT Found (from TS-3L backup)
The actual STARTNET.BAT on DOS machines loads network drivers manually:
```
LH /L:0;1,45472 /S c:\net\smartdrv.exe /q
c:\net\net initialize
c:\net\netbind.com
lh c:\net\umb.com
c:\net\tcptsr.exe
c:\net\tinyrfc.exe
c:\net\nmtsr.exe
c:\net\emsbfr.exe
c:\net\net start
net use T: \\d2testnas\test
net use X: \\d2testnas\datasheets
```
- `net start` prompts for computer name (pre-populated from SYSTEM.INI)
- Could add `/y` flag to suppress prompt, or use MACHINE variable
- Our v2.0 STARTNET.BAT on ProdSW is a simplified rewrite that was never deployed to machines
### T:\UPDATE.BAT
- Tiny 4-line wrapper at root of test share: `CALL T:\COMMON\ProdSW\DEPLOY.BAT %1`
- Allows running `T:\UPDATE TS-3L` from DOS machines
### Rsync Transfer Status
- Single stream still running from old NAS (.117) to new VM (.9)
- Snapshot transfer still pending restart
### Files Modified This Update
- `D:\ClaudeTools\projects\dataforth-dos\batch-files\DEPLOY.BAT` - v4.1 (trailing space fix)
- Deployed to NAS at `/data/test/COMMON/ProdSW/DEPLOY.BAT`
---
## Pending/Incomplete Tasks (Updated)
### Immediate
1. **Re-deploy TS-3L with DEPLOY v4.1** - needs new deploy + reboot to fix trailing space
2. **Set up AD2 rsync scheduled task** - Sync-FromNAS-rsync.ps1 deployed but no task created (15-min interval planned)
3. **Run real (non-dry) sync** of Sync-FromNAS-rsync.ps1 on AD2
4. **Database ingestion pipeline** - No ingestion exists yet. Data flows: NAS -> AD2 -> MariaDB @ 172.16.3.30
### After Data Transfer Complete
5. **Monitor old NAS rsync completion** - single stream still running
6. **Restart snapshot transfer** after data completes
7. **Verify data integrity** - compare file counts between old and new NAS
8. **Power off old NAS** once confirmed
### Batch File Updates Needed
9. **UPDATE.BAT** (in ProdSW) - has IF EXIST checks, needs v4.0 treatment
10. **ATESYNC.BAT / ATESYNCD.BAT** - have IF EXIST checks, need v4.0 treatment (not currently called by AUTOEXEC)
11. **STARTNET.BAT** - consider deploying updated version or adding `/y` to suppress net start prompt
12. **AV exclusions on AD2** - add exclusions for C:\Shares\test\ and rsync.exe

View File

@@ -0,0 +1,664 @@
# Sync-FromNAS-rsync.ps1
# Bidirectional sync between AD2 and NAS (D2TESTNAS) using rsync daemon
#
# PULL (NAS -> AD2): Test results (LOGS/*.DAT, Reports/*.TXT) -> Database import
# PUSH (AD2 -> NAS): Software updates (ProdSW/*, TODO.BAT) -> DOS machines
#
# Rsync daemon on NAS: port 873, module "test" maps to /data/test
# Rsync on AD2: cwRsync installed via Chocolatey (rsync.exe in PATH)
#
# Run: powershell -ExecutionPolicy Bypass -File C:\Shares\test\scripts\Sync-FromNAS-rsync.ps1
# Scheduled: Every 15 minutes via Windows Task Scheduler
param(
[switch]$DryRun, # Show what would be done without doing it
[switch]$Verbose # Extra output
)
# ============================================================================
# Configuration
# ============================================================================
$NAS_IP = "192.168.0.9"
$RSYNC_USER = "rsync"
$RSYNC_PASSWORD = "IQ203s32119"
$RSYNC_MODULE = "test"
$RSYNC_BASE = "rsync://${RSYNC_USER}@${NAS_IP}/${RSYNC_MODULE}"
$AD2_TEST_PATH = "C:\Shares\test"
$AD2_CYGDRIVE = "/cygdrive/c/Shares/test"
$LOG_FILE = "C:\Shares\test\scripts\sync-from-nas.log"
$STATUS_FILE = "C:\Shares\test\_SYNC_STATUS.txt"
$LOG_TYPES = @("5BLOG", "7BLOG", "8BLOG", "DSCLOG", "SCTLOG", "VASLOG", "PWRLOG", "HVLOG")
# Database import configuration
$IMPORT_SCRIPT = "C:\Shares\testdatadb\database\import.js"
$NODE_PATH = "node"
# Rsync timeout (seconds) - protects against NAS being unreachable
$RSYNC_TIMEOUT = 30
$RSYNC_CONTIMEOUT = 15
# ============================================================================
# Functions
# ============================================================================
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "$timestamp : $Message"
# Retry with brief delay if log file is locked by another process
for ($i = 0; $i -lt 3; $i++) {
try {
Add-Content -Path $LOG_FILE -Value $logLine -ErrorAction Stop
break
} catch {
Start-Sleep -Milliseconds 100
}
}
if ($Verbose) { Write-Host $logLine }
}
function Test-RsyncAvailable {
# Check that rsync.exe is available in PATH
$rsyncCmd = Get-Command "rsync" -ErrorAction SilentlyContinue
if (-not $rsyncCmd) {
Write-Log "FATAL: rsync.exe not found in PATH. Install cwRsync via Chocolatey: choco install rsync"
Write-Host "FATAL: rsync.exe not found in PATH. Install cwRsync via Chocolatey: choco install rsync" -ForegroundColor Red
return $false
}
Write-Log "Using rsync: $($rsyncCmd.Source)"
return $true
}
function ConvertTo-CygPath {
# Convert a Windows path like C:\Shares\test\TS-01\LOGS to /cygdrive/c/Shares/test/TS-01/LOGS
param([string]$WindowsPath)
$path = $WindowsPath -replace '\\', '/'
if ($path -match '^([A-Za-z]):(.*)$') {
$drive = $Matches[1].ToLower()
$rest = $Matches[2]
return "/cygdrive/$drive$rest"
}
return $path
}
function Invoke-Rsync {
# Execute rsync with the daemon password set via environment variable.
# Returns a hashtable with ExitCode and Output.
param(
[string[]]$Arguments
)
$env:RSYNC_PASSWORD = $RSYNC_PASSWORD
try {
$output = & rsync @Arguments 2>&1
$exitCode = $LASTEXITCODE
return @{
ExitCode = $exitCode
Output = $output
}
} catch {
return @{
ExitCode = 1
Output = "Exception invoking rsync: $_"
}
} finally {
$env:RSYNC_PASSWORD = $null
}
}
function Get-NASStationFolders {
# List station folders (TS-*) on the NAS using rsync --list-only.
# Returns an array of station folder names (e.g., "TS-01", "TS-02").
$result = Invoke-Rsync -Arguments @(
"--list-only",
"--timeout=$RSYNC_CONTIMEOUT",
"${RSYNC_BASE}/"
)
if ($result.ExitCode -ne 0) {
Write-Log "ERROR: Failed to list NAS root (exit $($result.ExitCode))"
$errText = $result.Output | Out-String
if ($errText.Trim()) { Write-Log " $errText" }
return @()
}
$stations = @()
foreach ($line in $result.Output) {
$lineStr = "$line".Trim()
# rsync --list-only output: "drwxrwxrwx 4,096 2026/03/10 14:30:00 TS-01"
# We want directory entries (starting with 'd') matching TS-*
if ($lineStr -match '^d' -and $lineStr -match '\s(TS-\S+)\s*$') {
$stations += $Matches[1]
}
}
return $stations
}
function Test-8dot3Name {
param([string]$Name)
# Returns $true if filename is 8.3 compatible (no spaces, name<=8, ext<=3)
if ($Name -match '[ ()\[\]{}]') { return $false }
$parts = $Name -split '\.'
if ($parts.Count -gt 2) { return $false }
if ($parts[0].Length -gt 8 -or $parts[0].Length -eq 0) { return $false }
if ($parts.Count -eq 2 -and $parts[1].Length -gt 3) { return $false }
return $true
}
function Test-8dot3Path {
param([string]$RelativePath)
# Check every component of the path is 8.3 compatible
$segments = $RelativePath -replace '\\', '/' -split '/'
foreach ($seg in $segments) {
if ($seg -eq '') { continue }
if (-not (Test-8dot3Name $seg)) { return $false }
}
return $true
}
function Import-ToDatabase {
param([string[]]$FilePaths)
if ($FilePaths.Count -eq 0) { return }
Write-Log "Importing $($FilePaths.Count) file(s) to database..."
# Build argument list
$importArgs = @("$IMPORT_SCRIPT", "--file") + $FilePaths
try {
$output = & $NODE_PATH $importArgs 2>&1
foreach ($line in $output) {
Write-Log " [DB] $line"
}
Write-Log "Database import complete"
} catch {
Write-Log "ERROR: Database import failed: $_"
}
}
# ============================================================================
# Main Script
# ============================================================================
Write-Log "=========================================="
Write-Log "Starting sync (rsync mode)"
if ($DryRun) { Write-Log "DRY RUN - no changes will be made" }
# -- Preflight: verify rsync is installed --
if (-not (Test-RsyncAvailable)) {
exit 2
}
$errorCount = 0
$syncedFiles = 0
$skippedFiles = 0
$syncedDatFiles = @() # Track DAT files for database import
$pushedFiles = 0
# ============================================================================
# PULL: NAS -> AD2 (Test Results)
# ============================================================================
Write-Log "--- NAS to AD2 Sync (Test Results) ---"
# Step 1: Enumerate station folders on NAS
Write-Log "Enumerating station folders on NAS..."
$nasStations = Get-NASStationFolders
if ($nasStations.Count -eq 0) {
Write-Log "WARNING: No station folders found on NAS (NAS may be unreachable)"
} else {
Write-Log "Found $($nasStations.Count) station folder(s): $($nasStations -join ', ')"
}
# Step 2: Pull DAT files per station per log type
foreach ($station in $nasStations) {
# Guard: if a file with this station name exists locally, skip it
$stationPath = "$AD2_TEST_PATH\$station"
if ((Test-Path $stationPath) -and -not (Test-Path $stationPath -PathType Container)) {
Write-Log "WARNING: '$station' exists as a file on AD2, not a directory - skipping (rename or delete the stray file)"
continue
}
foreach ($logType in $LOG_TYPES) {
$nasPath = "${RSYNC_BASE}/${station}/LOGS/${logType}/"
$localDir = "$AD2_TEST_PATH\$station\LOGS\$logType"
$localCygDir = "$(ConvertTo-CygPath $localDir)/"
# Ensure local directory exists (handle stray files blocking directory creation)
if (Test-Path $localDir) {
if (-not (Test-Path $localDir -PathType Container)) {
$strayName = "${localDir}.stray-file"
Write-Log " WARNING: '$station\LOGS\$logType' is a file, not directory - renaming to $(Split-Path $strayName -Leaf)"
Rename-Item -Path $localDir -NewName (Split-Path $strayName -Leaf) -Force
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}
} else {
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}
# Build rsync arguments
$rsyncArgs = @(
"--archive",
"--include=*.DAT",
"--exclude=*",
"--remove-source-files",
"--timeout=$RSYNC_TIMEOUT",
"--contimeout=$RSYNC_CONTIMEOUT",
"--itemize-changes"
)
if ($DryRun) {
$rsyncArgs += "--dry-run"
}
$rsyncArgs += $nasPath
$rsyncArgs += $localCygDir
if ($Verbose) {
Write-Log " rsync: $station/LOGS/$logType/"
}
$result = Invoke-Rsync -Arguments $rsyncArgs
if ($result.ExitCode -ne 0) {
# Exit code 23 = some files vanished (not fatal for us)
# Exit code 24 = partial transfer due to vanished source files
if ($result.ExitCode -in @(23, 24)) {
Write-Log " WARNING: rsync partial for $station/LOGS/$logType/ (exit $($result.ExitCode))"
} else {
$errText = $result.Output | Out-String
# If the path simply does not exist on NAS, rsync returns error but that is normal
# (not every station has every log type)
if ($errText -match "No such file or directory|does not exist|unknown module") {
if ($Verbose) {
Write-Log " (no $logType folder on NAS for $station)"
}
} else {
Write-Log " ERROR: rsync pull failed for $station/LOGS/$logType/ (exit $($result.ExitCode)): $errText"
$errorCount++
}
}
continue
}
# Parse itemized output to count transferred files
# Lines starting with ">f" indicate a file was received
foreach ($line in $result.Output) {
$lineStr = "$line".Trim()
if ($lineStr -match '^>f.*\s(\S+\.DAT)$') {
$fileName = $Matches[1]
$localFile = Join-Path $localDir $fileName
Write-Log " Pulled: $station/LOGS/$logType/$fileName"
$syncedDatFiles += $localFile
$syncedFiles++
}
}
}
# Pull TXT reports for this station
$nasReportsPath = "${RSYNC_BASE}/${station}/Reports/"
$localReportsDir = "$AD2_TEST_PATH\$station\Reports"
$localReportsCygDir = "$(ConvertTo-CygPath $localReportsDir)/"
# Ensure local directory exists
if (-not (Test-Path $localReportsDir)) {
New-Item -ItemType Directory -Path $localReportsDir -Force | Out-Null
}
$rsyncArgs = @(
"--archive",
"--include=*.TXT",
"--exclude=*",
"--remove-source-files",
"--timeout=$RSYNC_TIMEOUT",
"--contimeout=$RSYNC_CONTIMEOUT",
"--itemize-changes"
)
if ($DryRun) {
$rsyncArgs += "--dry-run"
}
$rsyncArgs += $nasReportsPath
$rsyncArgs += $localReportsCygDir
$result = Invoke-Rsync -Arguments $rsyncArgs
if ($result.ExitCode -ne 0) {
if ($result.ExitCode -in @(23, 24)) {
Write-Log " WARNING: rsync partial for $station/Reports/ (exit $($result.ExitCode))"
} else {
$errText = $result.Output | Out-String
if ($errText -match "No such file or directory|does not exist|unknown module") {
if ($Verbose) {
Write-Log " (no Reports folder on NAS for $station)"
}
} else {
Write-Log " ERROR: rsync pull failed for $station/Reports/ (exit $($result.ExitCode)): $errText"
$errorCount++
}
}
} else {
foreach ($line in $result.Output) {
$lineStr = "$line".Trim()
if ($lineStr -match '^>f.*\s(\S+\.TXT)$') {
$fileName = $Matches[1]
Write-Log " Pulled report: $station/Reports/$fileName"
$syncedFiles++
}
}
}
}
Write-Log "NAS to AD2 sync: $syncedFiles file(s) pulled"
# ============================================================================
# Import synced DAT files to database
# ============================================================================
if (-not $DryRun -and $syncedDatFiles.Count -gt 0) {
Import-ToDatabase -FilePaths $syncedDatFiles
}
# ============================================================================
# PUSH: AD2 -> NAS (Software Updates for DOS Machines)
# ============================================================================
Write-Log "--- AD2 to NAS Sync (Software Updates) ---"
# --- Helper: Push a local directory to a NAS path using rsync --update ---
# Only pushes files with 8.3-compatible paths.
# For directories, we use a filter approach: rsync the whole tree but pre-filter
# to 8.3-only names via a temp filter file or by iterating. Since rsync daemon
# does not need SSH, we can push entire directories at once for efficiency.
# However, to enforce 8.3 filtering, we use --files-from with a generated list.
function Push-DirectoryToNAS {
param(
[string]$LocalDir, # Windows path to local directory
[string]$NASPath, # rsync daemon path (e.g., rsync://rsync@.../test/COMMON/ProdSW/)
[switch]$Recurse,
[switch]$UpdateOnly # Use --update (skip files that are newer on receiver)
)
if (-not (Test-Path $LocalDir)) {
Write-Log " Path not found: $LocalDir"
return 0
}
$pushed = 0
$getChildArgs = @{
Path = $LocalDir
File = $true
ErrorAction = "SilentlyContinue"
}
if ($Recurse) { $getChildArgs["Recurse"] = $true }
$files = Get-ChildItem @getChildArgs
if (-not $files -or $files.Count -eq 0) {
if ($Verbose) { Write-Log " No files in $LocalDir" }
return 0
}
# Build a list of 8.3-compatible relative paths
$validFiles = @()
foreach ($file in $files) {
$relativePath = $file.FullName.Substring($LocalDir.Length + 1).Replace('\', '/')
if (-not (Test-8dot3Path $relativePath)) {
Write-Log " Skipping (non-8.3): $relativePath"
$script:skippedFiles++
continue
}
$validFiles += $relativePath
}
if ($validFiles.Count -eq 0) {
return 0
}
# Write valid file list to a temp file for --files-from
$tempFileList = "$env:TEMP\rsync-push-list-$(Get-Date -Format 'yyyyMMddHHmmss')-$([System.IO.Path]::GetRandomFileName()).txt"
$validFiles | Set-Content -Path $tempFileList -Encoding ASCII
$localCygDir = "$(ConvertTo-CygPath $LocalDir)/"
$tempCygPath = ConvertTo-CygPath $tempFileList
$rsyncArgs = @(
"--archive",
"--files-from=$tempCygPath",
"--timeout=$RSYNC_TIMEOUT",
"--contimeout=$RSYNC_CONTIMEOUT",
"--itemize-changes"
)
if ($UpdateOnly) {
$rsyncArgs += "--update"
}
if ($DryRun) {
$rsyncArgs += "--dry-run"
}
$rsyncArgs += $localCygDir
$rsyncArgs += $NASPath
$result = Invoke-Rsync -Arguments $rsyncArgs
# Clean up temp file
Remove-Item $tempFileList -ErrorAction SilentlyContinue
if ($result.ExitCode -ne 0 -and $result.ExitCode -notin @(23, 24)) {
$errText = $result.Output | Out-String
Write-Log " ERROR: rsync push failed (exit $($result.ExitCode)): $errText"
$script:errorCount++
return 0
}
# Count transferred files from itemized output
foreach ($line in $result.Output) {
$lineStr = "$line".Trim()
# Lines starting with ">f" or "<f" indicate file transfer
if ($lineStr -match '^[><]f') {
$pushed++
}
}
# In dry-run mode, count valid files as "would push"
if ($DryRun -and $pushed -eq 0) {
# itemize-changes with dry-run still shows >f lines, so pushed should be accurate
# But if nothing was shown (all up to date), that is fine
}
return $pushed
}
# -- COMMON/ProdSW --
# AD2 uses both _COMMON and COMMON; NAS uses COMMON
$commonSources = @(
"$AD2_TEST_PATH\_COMMON\ProdSW",
"$AD2_TEST_PATH\COMMON\ProdSW"
)
foreach ($commonDir in $commonSources) {
if (Test-Path $commonDir) {
Write-Log "Syncing COMMON ProdSW from: $commonDir"
$count = Push-DirectoryToNAS -LocalDir $commonDir -NASPath "${RSYNC_BASE}/COMMON/ProdSW/" -UpdateOnly
$pushedFiles += $count
Write-Log " Pushed $count file(s) from COMMON/ProdSW"
}
}
# -- Ate/ProdSW --
Write-Log "Syncing Ate/ProdSW data folders..."
$ateProdSwPath = "$AD2_TEST_PATH\Ate\ProdSW"
if (Test-Path $ateProdSwPath) {
$count = Push-DirectoryToNAS -LocalDir $ateProdSwPath -NASPath "${RSYNC_BASE}/Ate/ProdSW/" -Recurse -UpdateOnly
$pushedFiles += $count
Write-Log " Pushed $count file(s) from Ate/ProdSW"
} else {
Write-Log " Ate/ProdSW not found: $ateProdSwPath"
}
# -- UPDATE.BAT --
Write-Log "Syncing UPDATE.BAT..."
$updateBatLocal = "$AD2_TEST_PATH\UPDATE.BAT"
if (Test-Path $updateBatLocal) {
$localCyg = ConvertTo-CygPath $updateBatLocal
$rsyncArgs = @(
"--archive",
"--update",
"--timeout=$RSYNC_TIMEOUT",
"--contimeout=$RSYNC_CONTIMEOUT",
"--itemize-changes"
)
if ($DryRun) { $rsyncArgs += "--dry-run" }
$rsyncArgs += $localCyg
$rsyncArgs += "${RSYNC_BASE}/UPDATE.BAT"
$result = Invoke-Rsync -Arguments $rsyncArgs
if ($result.ExitCode -ne 0) {
$errText = $result.Output | Out-String
Write-Log " ERROR: Failed to push UPDATE.BAT (exit $($result.ExitCode)): $errText"
$errorCount++
} else {
foreach ($line in $result.Output) {
if ("$line".Trim() -match '^[><]f') {
Write-Log " Pushed: UPDATE.BAT"
$pushedFiles++
}
}
}
} else {
Write-Log " WARNING: UPDATE.BAT not found at $updateBatLocal"
}
# -- DEPLOY.BAT --
Write-Log "Syncing DEPLOY.BAT..."
$deployBatLocal = "$AD2_TEST_PATH\DEPLOY.BAT"
if (Test-Path $deployBatLocal) {
$localCyg = ConvertTo-CygPath $deployBatLocal
$rsyncArgs = @(
"--archive",
"--update",
"--timeout=$RSYNC_TIMEOUT",
"--contimeout=$RSYNC_CONTIMEOUT",
"--itemize-changes"
)
if ($DryRun) { $rsyncArgs += "--dry-run" }
$rsyncArgs += $localCyg
$rsyncArgs += "${RSYNC_BASE}/DEPLOY.BAT"
$result = Invoke-Rsync -Arguments $rsyncArgs
if ($result.ExitCode -ne 0) {
$errText = $result.Output | Out-String
Write-Log " ERROR: Failed to push DEPLOY.BAT (exit $($result.ExitCode)): $errText"
$errorCount++
} else {
foreach ($line in $result.Output) {
if ("$line".Trim() -match '^[><]f') {
Write-Log " Pushed: DEPLOY.BAT"
$pushedFiles++
}
}
}
} else {
Write-Log " WARNING: DEPLOY.BAT not found at $deployBatLocal"
}
# -- Per-station ProdSW and TODO.BAT --
Write-Log "Syncing station-specific ProdSW folders..."
$stationFolders = Get-ChildItem -Path $AD2_TEST_PATH -Directory -Filter "TS-*" -ErrorAction SilentlyContinue
foreach ($station in $stationFolders) {
# Skip station folders with non-8.3 names
if (-not (Test-8dot3Name $station.Name)) {
Write-Log " Skipping station (non-8.3 name): $($station.Name)"
continue
}
$prodSwPath = Join-Path $station.FullName "ProdSW"
if (Test-Path $prodSwPath) {
$count = Push-DirectoryToNAS -LocalDir $prodSwPath -NASPath "${RSYNC_BASE}/$($station.Name)/ProdSW/" -Recurse -UpdateOnly
if ($count -gt 0) {
Write-Log " Pushed $count file(s) for $($station.Name)/ProdSW"
}
$pushedFiles += $count
}
# TODO.BAT - one-shot: push then delete from AD2
$todoBatPath = Join-Path $station.FullName "TODO.BAT"
if (Test-Path $todoBatPath) {
Write-Log "Found TODO.BAT for $($station.Name)"
$localCyg = ConvertTo-CygPath $todoBatPath
$rsyncArgs = @(
"--archive",
"--timeout=$RSYNC_TIMEOUT",
"--contimeout=$RSYNC_CONTIMEOUT",
"--itemize-changes"
)
if ($DryRun) { $rsyncArgs += "--dry-run" }
$rsyncArgs += $localCyg
$rsyncArgs += "${RSYNC_BASE}/$($station.Name)/TODO.BAT"
$result = Invoke-Rsync -Arguments $rsyncArgs
if ($result.ExitCode -ne 0) {
$errText = $result.Output | Out-String
Write-Log " ERROR: Failed to push TODO.BAT for $($station.Name) (exit $($result.ExitCode)): $errText"
$errorCount++
} else {
Write-Log " Pushed TODO.BAT to NAS for $($station.Name)"
$pushedFiles++
if (-not $DryRun) {
# Remove from AD2 after successful push (one-shot mechanism)
Remove-Item -Path $todoBatPath -Force
Write-Log " Removed TODO.BAT from AD2 (pushed to NAS)"
} else {
Write-Log " [DRY RUN] Would remove TODO.BAT from AD2 after push"
}
}
}
}
Write-Log "AD2 to NAS sync: $pushedFiles file(s) pushed"
# ============================================================================
# Update Status File
# ============================================================================
$status = if ($errorCount -eq 0) { "OK" } else { "ERRORS" }
$statusContent = @"
AD2 <-> NAS Bidirectional Sync Status (rsync)
===============================================
Timestamp: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Status: $status
PULL (NAS -> AD2 - Test Results):
Files Pulled: $syncedFiles
Files Skipped: $skippedFiles
DAT Files Imported to DB: $($syncedDatFiles.Count)
PUSH (AD2 -> NAS - Software Updates):
Files Pushed: $pushedFiles
Errors: $errorCount
"@
Set-Content -Path $STATUS_FILE -Value $statusContent
Write-Log "=========================================="
Write-Log "Sync complete: PULL=$syncedFiles, PUSH=$pushedFiles, Errors=$errorCount"
Write-Log "=========================================="
# Exit with error code if there were failures
if ($errorCount -gt 0) {
exit 1
} else {
exit 0
}

View File

@@ -0,0 +1,540 @@
# Sync-AD2-NAS.ps1 (formerly Sync-FromNAS.ps1)
# Bidirectional sync between AD2 and NAS (D2TESTNAS)
#
# PULL (NAS -> AD2): Test results (LOGS/*.DAT, Reports/*.TXT) -> Database import
# PUSH (AD2 -> NAS): Software updates (ProdSW/*, TODO.BAT) -> DOS machines
#
# Run: powershell -ExecutionPolicy Bypass -File C:\Shares\test\scripts\Sync-FromNAS.ps1
# Scheduled: Every 15 minutes via Windows Task Scheduler
param(
[switch]$DryRun, # Show what would be done without doing it
[switch]$Verbose, # Extra output
[int]$MaxAgeMinutes = 1440 # Default: files from last 24 hours (was 60 min, too aggressive)
)
# ============================================================================
# Configuration
# ============================================================================
$NAS_IP = "192.168.0.9"
$NAS_USER = "root"
$NAS_PASSWORD = "Paper123!@#-nas"
$NAS_HOSTKEY = "SHA256:5CVIPlqjLPxO8n48PKLAP99nE6XkEBAjTkaYmJAeOdA"
$NAS_DATA_PATH = "/data/test"
$AD2_TEST_PATH = "C:\Shares\test"
$AD2_HISTLOGS_PATH = "C:\Shares\test\Ate\HISTLOGS"
$SSH = "C:\Program Files\OpenSSH\ssh.exe"
$SCP = "C:\Program Files\OpenSSH\scp.exe"
$SSH_KEY = "C:\Users\sysadmin\.ssh\id_ed25519"
$LOG_FILE = "C:\Shares\test\scripts\sync-from-nas.log"
$STATUS_FILE = "C:\Shares\test\_SYNC_STATUS.txt"
$LOG_TYPES = @("5BLOG", "7BLOG", "8BLOG", "DSCLOG", "SCTLOG", "VASLOG", "PWRLOG", "HVLOG")
# Database import configuration
$IMPORT_SCRIPT = "C:\Shares\testdatadb\database\import.js"
$NODE_PATH = "node"
# ============================================================================
# Functions
# ============================================================================
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "$timestamp : $Message"
Add-Content -Path $LOG_FILE -Value $logLine
if ($Verbose) { Write-Host $logLine }
}
function Invoke-NASCommand {
param([string]$Command)
$result = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" $Command 2>&1
return $result
}
function Get-NASFileList {
param(
[string]$FindCommand,
[string]$ListLabel
)
# Generate file list on NAS side, write to temp file, pull via SCP
# Key fix: use *> $null to discard all PowerShell output streams, preventing
# the stdout buffer deadlock that occurs with & $SSH ... 2>&1
$remoteTemp = "/tmp/nas-file-list-${ListLabel}.txt"
# Step 1: Run find on NAS, redirect output to temp file on NAS
# We discard ALL PowerShell output (*> $null) - we only need the side effect
& $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" "$FindCommand > $remoteTemp 2>/dev/null" *> $null
# Step 2: Pull the list file via SCP
$localTemp = "$env:TEMP\nas-file-list-${ListLabel}-$(Get-Date -Format 'yyyyMMddHHmmss').txt"
& $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${remoteTemp}" "$localTemp" *> $null
# Step 3: Read locally and clean up
$fileList = Get-Content $localTemp -ErrorAction SilentlyContinue | Where-Object { $_.Trim() -ne '' }
Remove-Item $localTemp -ErrorAction SilentlyContinue
# Step 4: Clean up remote temp file (small output, safe with Invoke-NASCommand)
Invoke-NASCommand "rm -f $remoteTemp" | Out-Null
return $fileList
}
function Copy-FromNAS {
param(
[string]$RemotePath,
[string]$LocalPath
)
# Ensure local directory exists
$localDir = Split-Path -Parent $LocalPath
if (-not (Test-Path $localDir)) {
New-Item -ItemType Directory -Path $localDir -Force | Out-Null
}
# Escape spaces and special chars with backslashes for SCP remote path
$escapedPath = $RemotePath -replace '([ ()\[\]{}])', '\$1'
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "${NAS_USER}@${NAS_IP}:${escapedPath}" "$LocalPath" 2>&1
if ($LASTEXITCODE -ne 0) {
$errorMsg = $result | Out-String
Write-Log " SCP PULL ERROR (exit $LASTEXITCODE): $errorMsg"
}
return $LASTEXITCODE -eq 0
}
function Rename-OnNAS {
param([string]$RemotePath)
Invoke-NASCommand "mv '$RemotePath' '${RemotePath}.synced'" | Out-Null
}
function Copy-ToNAS {
param(
[string]$LocalPath,
[string]$RemotePath
)
# Ensure remote directory exists via SSH mkdir -p
$remoteDir = ($RemotePath -replace '[^/]+$', '').TrimEnd('/')
Invoke-NASCommand "mkdir -p '$remoteDir'" | Out-Null
# Escape spaces and special chars with backslashes for SCP remote path
$escapedPath = $RemotePath -replace '([ ()\[\]{}])', '\$1'
$result = & $SCP -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="C:\Shares\test\scripts\.ssh\known_hosts" "$LocalPath" "${NAS_USER}@${NAS_IP}:${escapedPath}" 2>&1
if ($LASTEXITCODE -ne 0) {
$errorMsg = $result | Out-String
Write-Log " SCP PUSH ERROR (exit $LASTEXITCODE): $errorMsg"
}
return $LASTEXITCODE -eq 0
}
function Test-8dot3Name {
param([string]$Name)
# Returns $true if filename is 8.3 compatible (no spaces, name<=8, ext<=3)
if ($Name -match '[ ()\[\]{}]') { return $false }
$parts = $Name -split '\.'
if ($parts.Count -gt 2) { return $false }
if ($parts[0].Length -gt 8 -or $parts[0].Length -eq 0) { return $false }
if ($parts.Count -eq 2 -and $parts[1].Length -gt 3) { return $false }
return $true
}
function Test-8dot3Path {
param([string]$RelativePath)
# Check every component of the path is 8.3 compatible
$segments = $RelativePath -replace '\\', '/' -split '/'
foreach ($seg in $segments) {
if ($seg -eq '') { continue }
if (-not (Test-8dot3Name $seg)) { return $false }
}
return $true
}
function Get-FileHash256 {
param([string]$FilePath)
if (Test-Path $FilePath) {
return (Get-FileHash -Path $FilePath -Algorithm SHA256).Hash
}
return $null
}
function Import-ToDatabase {
param([string[]]$FilePaths)
if ($FilePaths.Count -eq 0) { return }
Write-Log "Importing $($FilePaths.Count) file(s) to database..."
# Build argument list
$importArgs = @("$IMPORT_SCRIPT", "--file") + $FilePaths
try {
$output = & $NODE_PATH $importArgs 2>&1
foreach ($line in $output) {
Write-Log " [DB] $line"
}
Write-Log "Database import complete"
} catch {
Write-Log "ERROR: Database import failed: $_"
}
}
# ============================================================================
# Main Script
# ============================================================================
Write-Log "=========================================="
Write-Log "Starting sync from NAS"
Write-Log "Max age: $MaxAgeMinutes minutes"
if ($DryRun) { Write-Log "DRY RUN - no changes will be made" }
# Ensure .ssh directory exists for known_hosts
$sshDir = "C:\Shares\test\scripts\.ssh"
if (-not (Test-Path $sshDir)) {
New-Item -ItemType Directory -Path $sshDir -Force | Out-Null
Write-Log "Created .ssh directory: $sshDir"
}
$errorCount = 0
$syncedFiles = 0
$skippedFiles = 0
$syncedDatFiles = @() # Track DAT files for database import
# Find all DAT files on NAS modified within the time window
# Uses temp file approach to avoid SSH output buffer hang with large file counts
Write-Log "Finding DAT files on NAS..."
$findCommand = "find $NAS_DATA_PATH/TS-*/LOGS -name '*.DAT' -type f -mmin -$MaxAgeMinutes"
$datFiles = Get-NASFileList -FindCommand $findCommand -ListLabel "dat"
if (-not $datFiles -or $datFiles.Count -eq 0) {
Write-Log "No new DAT files found on NAS"
} else {
Write-Log "Found $($datFiles.Count) DAT file(s) to process"
foreach ($remoteFile in $datFiles) {
$remoteFile = $remoteFile.Trim()
if ([string]::IsNullOrWhiteSpace($remoteFile)) { continue }
# Parse the path: /data/test/TS-XX/LOGS/7BLOG/file.DAT
if ($remoteFile -match "/data/test/(TS-[^/]+)/LOGS/([^/]+)/(.+\.DAT)$") {
$station = $Matches[1]
$logType = $Matches[2]
$fileName = $Matches[3]
Write-Log "Processing: $station/$logType/$fileName"
# Destination 1: Per-station folder (preserves structure)
$stationDest = Join-Path $AD2_TEST_PATH "$station\LOGS\$logType\$fileName"
# Destination 2: Aggregated HISTLOGS folder
$histlogsDest = Join-Path $AD2_HISTLOGS_PATH "$logType\$fileName"
if ($DryRun) {
Write-Log " [DRY RUN] Would copy to: $stationDest"
$syncedFiles++
} else {
# Copy to station folder only (skip HISTLOGS to avoid duplicates)
$success1 = Copy-FromNAS -RemotePath $remoteFile -LocalPath $stationDest
if ($success1) {
Write-Log " Copied to station folder"
# Rename on NAS after successful sync (preserves file as .synced)
Rename-OnNAS -RemotePath $remoteFile
Write-Log " Renamed to .synced on NAS"
# Track for database import
$syncedDatFiles += $stationDest
$syncedFiles++
} else {
Write-Log " ERROR: Failed to copy from NAS"
$errorCount++
}
}
} else {
Write-Log " Skipping (unexpected path format): $remoteFile"
$skippedFiles++
}
}
}
# Find and sync TXT report files
# Uses temp file approach to avoid SSH output buffer hang with large file counts
Write-Log "Finding TXT reports on NAS..."
$findReportsCommand = "find $NAS_DATA_PATH/TS-*/Reports -name '*.TXT' -type f -mmin -$MaxAgeMinutes"
$txtFiles = Get-NASFileList -FindCommand $findReportsCommand -ListLabel "txt"
if ($txtFiles -and $txtFiles.Count -gt 0) {
Write-Log "Found $($txtFiles.Count) TXT report(s) to process"
foreach ($remoteFile in $txtFiles) {
$remoteFile = $remoteFile.Trim()
if ([string]::IsNullOrWhiteSpace($remoteFile)) { continue }
if ($remoteFile -match "/data/test/(TS-[^/]+)/Reports/(.+\.TXT)$") {
$station = $Matches[1]
$fileName = $Matches[2]
Write-Log "Processing report: $station/$fileName"
# Destination: Per-station Reports folder
$reportDest = Join-Path $AD2_TEST_PATH "$station\Reports\$fileName"
if ($DryRun) {
Write-Log " [DRY RUN] Would copy to: $reportDest"
$syncedFiles++
} else {
$success = Copy-FromNAS -RemotePath $remoteFile -LocalPath $reportDest
if ($success) {
Write-Log " Copied report"
Rename-OnNAS -RemotePath $remoteFile
Write-Log " Renamed to .synced on NAS"
$syncedFiles++
} else {
Write-Log " ERROR: Failed to copy report"
$errorCount++
}
}
}
}
}
# ============================================================================
# Import synced DAT files to database
# ============================================================================
if (-not $DryRun -and $syncedDatFiles.Count -gt 0) {
Import-ToDatabase -FilePaths $syncedDatFiles
}
# ============================================================================
# PUSH: AD2 -> NAS (Software Updates for DOS Machines)
# ============================================================================
Write-Log "--- AD2 to NAS Sync (Software Updates) ---"
$pushedFiles = 0
# Sync COMMON/ProdSW (batch files for all stations)
# AD2 uses _COMMON, NAS uses COMMON - handle both
$commonSources = @(
@{ Local = "$AD2_TEST_PATH\_COMMON\ProdSW"; Remote = "$NAS_DATA_PATH/COMMON/ProdSW" },
@{ Local = "$AD2_TEST_PATH\COMMON\ProdSW"; Remote = "$NAS_DATA_PATH/COMMON/ProdSW" }
)
foreach ($source in $commonSources) {
if (Test-Path $source.Local) {
Write-Log "Syncing COMMON ProdSW from: $($source.Local)"
$commonFiles = Get-ChildItem -Path $source.Local -File -ErrorAction SilentlyContinue
foreach ($file in $commonFiles) {
if (-not (Test-8dot3Name $file.Name)) {
Write-Log " Skipping (non-8.3): $($file.Name)"
$skippedFiles++
continue
}
$remotePath = "$($source.Remote)/$($file.Name)"
if ($DryRun) {
Write-Log " [DRY RUN] Would push: $($file.Name) -> $remotePath"
$pushedFiles++
} else {
$success = Copy-ToNAS -LocalPath $file.FullName -RemotePath $remotePath
if ($success) {
Write-Log " Pushed: $($file.Name)"
$pushedFiles++
} else {
Write-Log " ERROR: Failed to push $($file.Name)"
$errorCount++
}
}
}
}
}
# Sync Ate/ProdSW (shared ATE data folders - 5BDATA, 7BDATA, 8BDATA, DSCDATA, etc.)
Write-Log "Syncing Ate/ProdSW data folders..."
$ateProdSwPath = "$AD2_TEST_PATH\Ate\ProdSW"
if (Test-Path $ateProdSwPath) {
# Get all files recursively (including subdirectories like DSCDATA, 5BDATA, etc.)
$ateFiles = Get-ChildItem -Path $ateProdSwPath -File -Recurse -ErrorAction SilentlyContinue
foreach ($file in $ateFiles) {
# Calculate relative path from Ate/ProdSW folder
$relativePath = $file.FullName.Substring($ateProdSwPath.Length + 1).Replace('\', '/')
if (-not (Test-8dot3Path $relativePath)) {
Write-Log " Skipping (non-8.3): Ate/ProdSW/$relativePath"
$skippedFiles++
continue
}
$remotePath = "$NAS_DATA_PATH/Ate/ProdSW/$relativePath"
if ($DryRun) {
Write-Log " [DRY RUN] Would push: Ate/ProdSW/$relativePath"
$pushedFiles++
} else {
$success = Copy-ToNAS -LocalPath $file.FullName -RemotePath $remotePath
if ($success) {
Write-Log " Pushed: Ate/ProdSW/$relativePath"
$pushedFiles++
} else {
Write-Log " ERROR: Failed to push Ate/ProdSW/$relativePath"
$errorCount++
}
}
}
} else {
Write-Log " Ate/ProdSW not found: $ateProdSwPath"
}
# Sync UPDATE.BAT (root level utility)
Write-Log "Syncing UPDATE.BAT..."
$updateBatLocal = "$AD2_TEST_PATH\UPDATE.BAT"
if (Test-Path $updateBatLocal) {
$updateBatRemote = "$NAS_DATA_PATH/UPDATE.BAT"
if ($DryRun) {
Write-Log " [DRY RUN] Would push: UPDATE.BAT -> $updateBatRemote"
$pushedFiles++
} else {
$success = Copy-ToNAS -LocalPath $updateBatLocal -RemotePath $updateBatRemote
if ($success) {
Write-Log " Pushed: UPDATE.BAT"
$pushedFiles++
} else {
Write-Log " ERROR: Failed to push UPDATE.BAT"
$errorCount++
}
}
} else {
Write-Log " WARNING: UPDATE.BAT not found at $updateBatLocal"
}
# Sync DEPLOY.BAT (root level utility)
Write-Log "Syncing DEPLOY.BAT..."
$deployBatLocal = "$AD2_TEST_PATH\DEPLOY.BAT"
if (Test-Path $deployBatLocal) {
$deployBatRemote = "$NAS_DATA_PATH/DEPLOY.BAT"
if ($DryRun) {
Write-Log " [DRY RUN] Would push: DEPLOY.BAT -> $deployBatRemote"
$pushedFiles++
} else {
$success = Copy-ToNAS -LocalPath $deployBatLocal -RemotePath $deployBatRemote
if ($success) {
Write-Log " Pushed: DEPLOY.BAT"
$pushedFiles++
} else {
Write-Log " ERROR: Failed to push DEPLOY.BAT"
$errorCount++
}
}
} else {
Write-Log " WARNING: DEPLOY.BAT not found at $deployBatLocal"
}
# Sync per-station ProdSW folders
Write-Log "Syncing station-specific ProdSW folders..."
$stationFolders = Get-ChildItem -Path $AD2_TEST_PATH -Directory -Filter "TS-*" -ErrorAction SilentlyContinue
foreach ($station in $stationFolders) {
# Skip station folders with non-8.3 names (e.g. "TS-1 OBS")
if (-not (Test-8dot3Name $station.Name)) {
Write-Log " Skipping station (non-8.3 name): $($station.Name)"
continue
}
$prodSwPath = Join-Path $station.FullName "ProdSW"
if (Test-Path $prodSwPath) {
# Get all files in ProdSW (including subdirectories)
$prodSwFiles = Get-ChildItem -Path $prodSwPath -File -Recurse -ErrorAction SilentlyContinue
foreach ($file in $prodSwFiles) {
# Calculate relative path from ProdSW folder
$relativePath = $file.FullName.Substring($prodSwPath.Length + 1).Replace('\', '/')
if (-not (Test-8dot3Path $relativePath)) {
Write-Log " Skipping (non-8.3): $($station.Name)/ProdSW/$relativePath"
$skippedFiles++
continue
}
$remotePath = "$NAS_DATA_PATH/$($station.Name)/ProdSW/$relativePath"
if ($DryRun) {
Write-Log " [DRY RUN] Would push: $($station.Name)/ProdSW/$relativePath"
$pushedFiles++
} else {
$success = Copy-ToNAS -LocalPath $file.FullName -RemotePath $remotePath
if ($success) {
Write-Log " Pushed: $($station.Name)/ProdSW/$relativePath"
$pushedFiles++
} else {
Write-Log " ERROR: Failed to push $($station.Name)/ProdSW/$relativePath"
$errorCount++
}
}
}
}
# Check for TODO.BAT (one-time task file)
$todoBatPath = Join-Path $station.FullName "TODO.BAT"
if (Test-Path $todoBatPath) {
$remoteTodoPath = "$NAS_DATA_PATH/$($station.Name)/TODO.BAT"
Write-Log "Found TODO.BAT for $($station.Name)"
if ($DryRun) {
Write-Log " [DRY RUN] Would push TODO.BAT -> $remoteTodoPath"
$pushedFiles++
} else {
$success = Copy-ToNAS -LocalPath $todoBatPath -RemotePath $remoteTodoPath
if ($success) {
Write-Log " Pushed TODO.BAT to NAS"
# Remove from AD2 after successful push (one-shot mechanism)
Remove-Item -Path $todoBatPath -Force
Write-Log " Removed TODO.BAT from AD2 (pushed to NAS)"
$pushedFiles++
} else {
Write-Log " ERROR: Failed to push TODO.BAT"
$errorCount++
}
}
}
}
Write-Log "AD2 to NAS sync: $pushedFiles file(s) pushed"
# ============================================================================
# Update Status File
# ============================================================================
$status = if ($errorCount -eq 0) { "OK" } else { "ERRORS" }
$statusContent = @"
AD2 <-> NAS Bidirectional Sync Status
======================================
Timestamp: $(Get-Date -Format "yyyy-MM-dd HH:mm:ss")
Status: $status
PULL (NAS -> AD2 - Test Results):
Files Pulled: $syncedFiles
Files Skipped: $skippedFiles
DAT Files Imported to DB: $($syncedDatFiles.Count)
PUSH (AD2 -> NAS - Software Updates):
Files Pushed: $pushedFiles
Errors: $errorCount
"@
Set-Content -Path $STATUS_FILE -Value $statusContent
Write-Log "=========================================="
Write-Log "Sync complete: PULL=$syncedFiles, PUSH=$pushedFiles, Errors=$errorCount"
Write-Log "=========================================="
# Exit with error code if there were failures
if ($errorCount -gt 0) {
exit 1
} else {
exit 0
}

View File

@@ -0,0 +1,302 @@
# deploy-sync-fixes.ps1
# Deploys patched Sync-FromNAS.ps1 and import.js to AD2 (192.168.0.6)
#
# Fixes deployed:
# 1. Sync-FromNAS.ps1 - PULL hang fix (temp file approach for large find output)
# 2. Sync-FromNAS.ps1 - SCP quoting fix (handles spaces, parens, special chars)
# 3. Sync-FromNAS.ps1 - Rename-OnNAS replaces Remove-FromNAS (.synced instead of delete)
# 4. import.js - INSERT OR REPLACE instead of INSERT OR IGNORE (re-tests keep latest)
#
# Run from: Mike's workstation (where the patched files live)
# Target: AD2 (192.168.0.6) as INTRANET\sysadmin
param(
[switch]$DryRun # Show what would be done without doing it
)
# ============================================================================
# Configuration
# ============================================================================
$AD2_IP = "192.168.0.6"
$AD2_USER = "INTRANET\sysadmin"
$AD2_PASSWORD = "Paper123!@#"
$SSH = "C:\Windows\System32\OpenSSH\ssh.exe"
$SCP = "C:\Windows\System32\OpenSSH\scp.exe"
# Source files (local to this machine)
$SCRIPT_DIR = Split-Path -Parent $MyInvocation.MyCommand.Path
$LOCAL_SYNC_SCRIPT = Join-Path $SCRIPT_DIR "Sync-FromNAS.ps1"
$LOCAL_IMPORT_SCRIPT = Join-Path $SCRIPT_DIR "import.js"
# Destination paths on AD2
$REMOTE_SYNC_PATH = "C:\Shares\test\scripts\Sync-FromNAS.ps1"
$REMOTE_IMPORT_PATH = "C:\Shares\testdatadb\database\import.js"
$REMOTE_SSH_DIR = "C:\Shares\test\scripts\.ssh"
$DATE_SUFFIX = Get-Date -Format "yyyyMMdd"
# ============================================================================
# Functions
# ============================================================================
function Write-Status {
param(
[string]$Marker,
[string]$Message
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Write-Host "$timestamp [$Marker] $Message"
}
function Invoke-AD2Command {
param([string]$Command)
$result = & $SSH -o StrictHostKeyChecking=accept-new -o ConnectTimeout=10 "${AD2_USER}@${AD2_IP}" $Command 2>&1
return $result
}
function Copy-ToAD2 {
param(
[string]$LocalPath,
[string]$RemotePath
)
# SCP to AD2 - quote remote path for Windows paths with backslashes
$result = & $SCP -O -o StrictHostKeyChecking=accept-new "$LocalPath" "${AD2_USER}@${AD2_IP}:`"${RemotePath}`"" 2>&1
if ($LASTEXITCODE -ne 0) {
$errorMsg = $result | Out-String
Write-Status "ERROR" "SCP failed (exit $LASTEXITCODE): $errorMsg"
return $false
}
return $true
}
# ============================================================================
# Pre-flight Checks
# ============================================================================
Write-Host ""
Write-Host "============================================"
Write-Host " Dataforth Sync Fixes - Deployment Script"
Write-Host "============================================"
Write-Host ""
if ($DryRun) {
Write-Status "INFO" "DRY RUN - no changes will be made"
Write-Host ""
}
# Verify source files exist
Write-Status "INFO" "Checking source files..."
if (-not (Test-Path $LOCAL_SYNC_SCRIPT)) {
Write-Status "ERROR" "Source file not found: $LOCAL_SYNC_SCRIPT"
exit 1
}
Write-Status "OK" "Found: $LOCAL_SYNC_SCRIPT"
if (-not (Test-Path $LOCAL_IMPORT_SCRIPT)) {
Write-Status "ERROR" "Source file not found: $LOCAL_IMPORT_SCRIPT"
exit 1
}
Write-Status "OK" "Found: $LOCAL_IMPORT_SCRIPT"
# Verify AD2 is reachable
Write-Status "INFO" "Testing connectivity to AD2 ($AD2_IP)..."
$pingResult = Test-Connection -ComputerName $AD2_IP -Count 1 -Quiet
if (-not $pingResult) {
Write-Status "ERROR" "Cannot reach AD2 at $AD2_IP"
exit 1
}
Write-Status "OK" "AD2 is reachable"
# Test SSH connection
Write-Status "INFO" "Testing SSH connection..."
$sshTest = Invoke-AD2Command "echo CONNECTION_OK"
$sshTestStr = $sshTest | Out-String
if ($sshTestStr -notmatch "CONNECTION_OK") {
Write-Status "ERROR" "SSH connection failed: $sshTestStr"
exit 1
}
Write-Status "OK" "SSH connection established"
# ============================================================================
# Step 1: Create backups on AD2
# ============================================================================
Write-Host ""
Write-Status "INFO" "--- Step 1: Creating backups on AD2 ---"
if ($DryRun) {
Write-Status "INFO" "[DRY RUN] Would backup $REMOTE_SYNC_PATH -> ${REMOTE_SYNC_PATH}.bak-${DATE_SUFFIX}"
Write-Status "INFO" "[DRY RUN] Would backup $REMOTE_IMPORT_PATH -> ${REMOTE_IMPORT_PATH}.bak-${DATE_SUFFIX}"
} else {
# Backup Sync-FromNAS.ps1
$backupCmd1 = "copy `"$REMOTE_SYNC_PATH`" `"${REMOTE_SYNC_PATH}.bak-${DATE_SUFFIX}`""
$backupResult1 = Invoke-AD2Command "cmd /c $backupCmd1"
if ($LASTEXITCODE -eq 0) {
Write-Status "OK" "Backed up Sync-FromNAS.ps1 -> .bak-${DATE_SUFFIX}"
} else {
Write-Status "WARNING" "Backup of Sync-FromNAS.ps1 may have failed (file might not exist yet): $($backupResult1 | Out-String)"
}
# Backup import.js
$backupCmd2 = "copy `"$REMOTE_IMPORT_PATH`" `"${REMOTE_IMPORT_PATH}.bak-${DATE_SUFFIX}`""
$backupResult2 = Invoke-AD2Command "cmd /c $backupCmd2"
if ($LASTEXITCODE -eq 0) {
Write-Status "OK" "Backed up import.js -> .bak-${DATE_SUFFIX}"
} else {
Write-Status "WARNING" "Backup of import.js may have failed (file might not exist yet): $($backupResult2 | Out-String)"
}
}
# ============================================================================
# Step 2: Create .ssh directory on AD2 if missing
# ============================================================================
Write-Host ""
Write-Status "INFO" "--- Step 2: Ensuring .ssh directory exists ---"
if ($DryRun) {
Write-Status "INFO" "[DRY RUN] Would create $REMOTE_SSH_DIR if missing"
} else {
$mkdirCmd = "cmd /c if not exist `"$REMOTE_SSH_DIR`" mkdir `"$REMOTE_SSH_DIR`""
Invoke-AD2Command $mkdirCmd | Out-Null
Write-Status "OK" "Ensured .ssh directory exists: $REMOTE_SSH_DIR"
# Create empty known_hosts if it does not exist
$knownHostsPath = "$REMOTE_SSH_DIR\known_hosts"
$touchCmd = "cmd /c if not exist `"$knownHostsPath`" type nul > `"$knownHostsPath`""
Invoke-AD2Command $touchCmd | Out-Null
Write-Status "OK" "Ensured known_hosts file exists: $knownHostsPath"
}
# ============================================================================
# Step 3: Deploy patched files
# ============================================================================
Write-Host ""
Write-Status "INFO" "--- Step 3: Deploying patched files ---"
if ($DryRun) {
Write-Status "INFO" "[DRY RUN] Would SCP $LOCAL_SYNC_SCRIPT -> ${AD2_IP}:${REMOTE_SYNC_PATH}"
Write-Status "INFO" "[DRY RUN] Would SCP $LOCAL_IMPORT_SCRIPT -> ${AD2_IP}:${REMOTE_IMPORT_PATH}"
} else {
# Deploy Sync-FromNAS.ps1
Write-Status "INFO" "Deploying Sync-FromNAS.ps1..."
$success1 = Copy-ToAD2 -LocalPath $LOCAL_SYNC_SCRIPT -RemotePath $REMOTE_SYNC_PATH
if ($success1) {
Write-Status "OK" "Deployed Sync-FromNAS.ps1"
} else {
Write-Status "ERROR" "Failed to deploy Sync-FromNAS.ps1"
exit 1
}
# Deploy import.js
Write-Status "INFO" "Deploying import.js..."
$success2 = Copy-ToAD2 -LocalPath $LOCAL_IMPORT_SCRIPT -RemotePath $REMOTE_IMPORT_PATH
if ($success2) {
Write-Status "OK" "Deployed import.js"
} else {
Write-Status "ERROR" "Failed to deploy import.js"
exit 1
}
}
# ============================================================================
# Step 4: Verify deployment
# ============================================================================
Write-Host ""
Write-Status "INFO" "--- Step 4: Verifying deployment ---"
if ($DryRun) {
Write-Status "INFO" "[DRY RUN] Would verify files exist on AD2"
} else {
# Check Sync-FromNAS.ps1 exists and has content
$verifyCmd1 = "cmd /c if exist `"$REMOTE_SYNC_PATH`" (echo FILE_EXISTS) else (echo FILE_MISSING)"
$verify1 = Invoke-AD2Command $verifyCmd1 | Out-String
if ($verify1 -match "FILE_EXISTS") {
Write-Status "OK" "Verified: Sync-FromNAS.ps1 exists on AD2"
} else {
Write-Status "ERROR" "Verification failed: Sync-FromNAS.ps1 not found on AD2"
exit 1
}
# Check import.js exists
$verifyCmd2 = "cmd /c if exist `"$REMOTE_IMPORT_PATH`" (echo FILE_EXISTS) else (echo FILE_MISSING)"
$verify2 = Invoke-AD2Command $verifyCmd2 | Out-String
if ($verify2 -match "FILE_EXISTS") {
Write-Status "OK" "Verified: import.js exists on AD2"
} else {
Write-Status "ERROR" "Verification failed: import.js not found on AD2"
exit 1
}
}
# ============================================================================
# Step 5: Quick dry-run test of the sync script
# ============================================================================
Write-Host ""
Write-Status "INFO" "--- Step 5: Running dry-run test of sync script ---"
if ($DryRun) {
Write-Status "INFO" "[DRY RUN] Would run: powershell -ExecutionPolicy Bypass -File $REMOTE_SYNC_PATH -DryRun"
} else {
Write-Status "INFO" "Executing sync script in dry-run mode on AD2..."
$testCmd = "powershell -ExecutionPolicy Bypass -File `"$REMOTE_SYNC_PATH`" -DryRun -Verbose"
$testResult = Invoke-AD2Command $testCmd
$testOutput = $testResult | Out-String
# Check if the script ran without critical errors
if ($LASTEXITCODE -eq 0) {
Write-Status "OK" "Sync script dry-run completed successfully"
if ($testOutput.Trim().Length -gt 0) {
Write-Host ""
Write-Host " --- Dry-run output ---"
foreach ($line in ($testResult | Select-Object -First 20)) {
Write-Host " $line"
}
$totalLines = ($testResult | Measure-Object).Count
if ($totalLines -gt 20) {
Write-Host " ... ($($totalLines - 20) more lines)"
}
Write-Host " --- End dry-run output ---"
}
} else {
Write-Status "WARNING" "Sync script dry-run exited with code $LASTEXITCODE"
Write-Status "INFO" "This may be expected if NAS is unreachable from AD2 during test"
if ($testOutput.Trim().Length -gt 0) {
Write-Host ""
Write-Host " --- Dry-run output ---"
foreach ($line in ($testResult | Select-Object -First 10)) {
Write-Host " $line"
}
Write-Host " --- End dry-run output ---"
}
}
}
# ============================================================================
# Summary
# ============================================================================
Write-Host ""
Write-Host "============================================"
Write-Host " Deployment Summary"
Write-Host "============================================"
Write-Host ""
if ($DryRun) {
Write-Status "INFO" "DRY RUN complete - no changes were made"
} else {
Write-Status "OK" "Sync-FromNAS.ps1 deployed to AD2 (backup: .bak-${DATE_SUFFIX})"
Write-Status "OK" "import.js deployed to AD2 (backup: .bak-${DATE_SUFFIX})"
Write-Status "OK" ".ssh directory and known_hosts verified"
Write-Status "OK" "Dry-run test executed"
Write-Host ""
Write-Status "INFO" "Fixes applied:"
Write-Host " 1. PULL hang fix: find output written to temp file, pulled via SCP"
Write-Host " 2. SCP quoting fix: remote paths quoted to handle special characters"
Write-Host " 3. Rename-OnNAS: files renamed to .synced instead of deleted"
Write-Host " 4. INSERT OR REPLACE: re-tested devices keep latest result"
Write-Host ""
Write-Status "INFO" "Next sync cycle will use the patched scripts automatically"
}
Write-Host ""
exit 0

View File

@@ -0,0 +1,395 @@
/**
* Data Import Script
* Imports test data from DAT and SHT files into SQLite database
*/
const fs = require('fs');
const path = require('path');
const Database = require('better-sqlite3');
const { parseMultilineFile, extractTestStation } = require('../parsers/multiline');
const { parseCsvFile } = require('../parsers/csvline');
const { parseShtFile } = require('../parsers/shtfile');
// Configuration
const DB_PATH = path.join(__dirname, 'testdata.db');
const SCHEMA_PATH = path.join(__dirname, 'schema.sql');
// Data source paths
const TEST_PATH = 'C:/Shares/test';
const RECOVERY_PATH = 'C:/Shares/Recovery-TEST';
const HISTLOGS_PATH = path.join(TEST_PATH, 'Ate/HISTLOGS');
// Log types and their parsers
const LOG_TYPES = {
'DSCLOG': { parser: 'multiline', ext: '.DAT' },
'5BLOG': { parser: 'multiline', ext: '.DAT' },
'8BLOG': { parser: 'multiline', ext: '.DAT' },
'PWRLOG': { parser: 'multiline', ext: '.DAT' },
'SCTLOG': { parser: 'multiline', ext: '.DAT' },
'VASLOG': { parser: 'multiline', ext: '.DAT' },
'7BLOG': { parser: 'csvline', ext: '.DAT' }
};
// Initialize database
function initDatabase() {
console.log('Initializing database...');
const db = new Database(DB_PATH);
// Read and execute schema
const schema = fs.readFileSync(SCHEMA_PATH, 'utf8');
db.exec(schema);
console.log('Database initialized.');
return db;
}
// Prepare insert statement
function prepareInsert(db) {
return db.prepare(`
INSERT OR REPLACE INTO test_records
(log_type, model_number, serial_number, test_date, test_station, overall_result, raw_data, source_file)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
}
// Find all files of a specific type in a directory
function findFiles(dir, pattern, recursive = true) {
const results = [];
try {
if (!fs.existsSync(dir)) return results;
const items = fs.readdirSync(dir, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dir, item.name);
if (item.isDirectory() && recursive) {
results.push(...findFiles(fullPath, pattern, recursive));
} else if (item.isFile()) {
if (pattern.test(item.name)) {
results.push(fullPath);
}
}
}
} catch (err) {
// Ignore permission errors
}
return results;
}
// Import records from a file
function importFile(db, insertStmt, filePath, logType, parser) {
let records = [];
const testStation = extractTestStation(filePath);
try {
switch (parser) {
case 'multiline':
records = parseMultilineFile(filePath, logType, testStation);
break;
case 'csvline':
records = parseCsvFile(filePath, testStation);
break;
case 'shtfile':
records = parseShtFile(filePath, testStation);
break;
}
let imported = 0;
for (const record of records) {
try {
const result = insertStmt.run(
record.log_type,
record.model_number,
record.serial_number,
record.test_date,
record.test_station,
record.overall_result,
record.raw_data,
record.source_file
);
if (result.changes > 0) imported++;
} catch (err) {
// Duplicate or constraint error - skip
}
}
return { total: records.length, imported };
} catch (err) {
console.error(`Error importing ${filePath}: ${err.message}`);
return { total: 0, imported: 0 };
}
}
// Import from HISTLOGS (master consolidated logs)
function importHistlogs(db, insertStmt) {
console.log('\n=== Importing from HISTLOGS ===');
let totalImported = 0;
let totalRecords = 0;
for (const [logType, config] of Object.entries(LOG_TYPES)) {
const logDir = path.join(HISTLOGS_PATH, logType);
if (!fs.existsSync(logDir)) {
console.log(` ${logType}: directory not found`);
continue;
}
const files = findFiles(logDir, new RegExp(`\\${config.ext}$`, 'i'), false);
console.log(` ${logType}: found ${files.length} files`);
for (const file of files) {
const { total, imported } = importFile(db, insertStmt, file, logType, config.parser);
totalRecords += total;
totalImported += imported;
}
}
console.log(` HISTLOGS total: ${totalImported} records imported (${totalRecords} parsed)`);
return totalImported;
}
// Import from test station logs
function importStationLogs(db, insertStmt, basePath, label) {
console.log(`\n=== Importing from ${label} ===`);
let totalImported = 0;
let totalRecords = 0;
// Find all test station directories (TS-1, TS-27, TS-8L, TS-10R, etc.)
const stationPattern = /^TS-\d+[LR]?$/i;
let stations = [];
try {
const items = fs.readdirSync(basePath, { withFileTypes: true });
stations = items
.filter(i => i.isDirectory() && stationPattern.test(i.name))
.map(i => i.name);
} catch (err) {
console.log(` Error reading ${basePath}: ${err.message}`);
return 0;
}
console.log(` Found stations: ${stations.join(', ')}`);
for (const station of stations) {
const logsDir = path.join(basePath, station, 'LOGS');
if (!fs.existsSync(logsDir)) continue;
for (const [logType, config] of Object.entries(LOG_TYPES)) {
const logDir = path.join(logsDir, logType);
if (!fs.existsSync(logDir)) continue;
const files = findFiles(logDir, new RegExp(`\\${config.ext}$`, 'i'), false);
for (const file of files) {
const { total, imported } = importFile(db, insertStmt, file, logType, config.parser);
totalRecords += total;
totalImported += imported;
}
}
}
// Also import SHT files
const shtFiles = findFiles(basePath, /\.SHT$/i, true);
console.log(` Found ${shtFiles.length} SHT files`);
for (const file of shtFiles) {
const { total, imported } = importFile(db, insertStmt, file, 'SHT', 'shtfile');
totalRecords += total;
totalImported += imported;
}
console.log(` ${label} total: ${totalImported} records imported (${totalRecords} parsed)`);
return totalImported;
}
// Import from Recovery-TEST backups (newest first)
function importRecoveryBackups(db, insertStmt) {
console.log('\n=== Importing from Recovery-TEST backups ===');
if (!fs.existsSync(RECOVERY_PATH)) {
console.log(' Recovery-TEST directory not found');
return 0;
}
// Get backup dates, sort newest first
const backups = fs.readdirSync(RECOVERY_PATH, { withFileTypes: true })
.filter(i => i.isDirectory() && /^\d{2}-\d{2}-\d{2}$/.test(i.name))
.map(i => i.name)
.sort()
.reverse();
console.log(` Found backup dates: ${backups.join(', ')}`);
let totalImported = 0;
for (const backup of backups) {
const backupPath = path.join(RECOVERY_PATH, backup);
const imported = importStationLogs(db, insertStmt, backupPath, `Recovery-TEST/${backup}`);
totalImported += imported;
}
return totalImported;
}
// Main import function
async function runImport() {
console.log('========================================');
console.log('Test Data Import');
console.log('========================================');
console.log(`Database: ${DB_PATH}`);
console.log(`Start time: ${new Date().toISOString()}`);
const db = initDatabase();
const insertStmt = prepareInsert(db);
let grandTotal = 0;
// Use transaction for performance
const importAll = db.transaction(() => {
// 1. Import HISTLOGS first (authoritative)
grandTotal += importHistlogs(db, insertStmt);
// 2. Import Recovery backups (newest first)
grandTotal += importRecoveryBackups(db, insertStmt);
// 3. Import current test folder
grandTotal += importStationLogs(db, insertStmt, TEST_PATH, 'test');
});
importAll();
// Get final stats
const stats = db.prepare('SELECT COUNT(*) as count FROM test_records').get();
console.log('\n========================================');
console.log('Import Complete');
console.log('========================================');
console.log(`Total records in database: ${stats.count}`);
console.log(`End time: ${new Date().toISOString()}`);
db.close();
}
// Import a single file (for incremental imports from sync)
function importSingleFile(filePath) {
console.log(`Importing: ${filePath}`);
const db = new Database(DB_PATH);
const insertStmt = prepareInsert(db);
// Determine log type from path
let logType = null;
let parser = null;
for (const [type, config] of Object.entries(LOG_TYPES)) {
if (filePath.includes(type)) {
logType = type;
parser = config.parser;
break;
}
}
if (!logType) {
// Check for SHT files
if (/\.SHT$/i.test(filePath)) {
logType = 'SHT';
parser = 'shtfile';
} else {
console.log(` Unknown log type for: ${filePath}`);
db.close();
return { total: 0, imported: 0 };
}
}
const result = importFile(db, insertStmt, filePath, logType, parser);
console.log(` Imported ${result.imported} of ${result.total} records`);
db.close();
return result;
}
// Import multiple files (for batch incremental imports)
function importFiles(filePaths) {
console.log(`\n========================================`);
console.log(`Incremental Import: ${filePaths.length} files`);
console.log(`========================================`);
const db = new Database(DB_PATH);
const insertStmt = prepareInsert(db);
let totalImported = 0;
let totalRecords = 0;
const importBatch = db.transaction(() => {
for (const filePath of filePaths) {
// Determine log type from path
let logType = null;
let parser = null;
for (const [type, config] of Object.entries(LOG_TYPES)) {
if (filePath.includes(type)) {
logType = type;
parser = config.parser;
break;
}
}
if (!logType) {
if (/\.SHT$/i.test(filePath)) {
logType = 'SHT';
parser = 'shtfile';
} else {
console.log(` Skipping unknown type: ${filePath}`);
continue;
}
}
const { total, imported } = importFile(db, insertStmt, filePath, logType, parser);
totalRecords += total;
totalImported += imported;
console.log(` ${path.basename(filePath)}: ${imported}/${total} records`);
}
});
importBatch();
console.log(`\nTotal: ${totalImported} records imported (${totalRecords} parsed)`);
db.close();
return { total: totalRecords, imported: totalImported };
}
// Run if called directly
if (require.main === module) {
// Check for command line arguments
const args = process.argv.slice(2);
if (args.length > 0 && args[0] === '--file') {
// Import specific file(s)
const files = args.slice(1);
if (files.length === 0) {
console.log('Usage: node import.js --file <file1> [file2] ...');
process.exit(1);
}
importFiles(files);
} else if (args.length > 0 && args[0] === '--help') {
console.log('Usage:');
console.log(' node import.js Full import from all sources');
console.log(' node import.js --file <f> Import specific file(s)');
process.exit(0);
} else {
// Full import
runImport().catch(console.error);
}
}
module.exports = { runImport, importSingleFile, importFiles };

View File

@@ -0,0 +1,61 @@
# backup-db.ps1
# Backs up the TestDataDB SQLite database with 7-day retention.
# Intended to run as a scheduled task via install-backup-task.ps1.
$ErrorActionPreference = 'Stop'
$SourceDb = 'C:\Shares\testdatadb\database\testdata.db'
$BackupDir = 'C:\Shares\testdatadb\backups'
$LogFile = 'C:\Shares\testdatadb\logs\backup.log'
$Retention = 7
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$entry = "[$timestamp] $Message"
Add-Content -Path $LogFile -Value $entry
Write-Host $entry
}
# Ensure directories exist
foreach ($dir in @($BackupDir, (Split-Path $LogFile -Parent))) {
if (-not (Test-Path $dir)) {
New-Item -ItemType Directory -Path $dir -Force | Out-Null
}
}
# Verify source database exists
if (-not (Test-Path $SourceDb)) {
Write-Log "[ERROR] Source database not found: $SourceDb"
exit 1
}
# Create dated backup
$datestamp = Get-Date -Format 'yyyy-MM-dd'
$backupFile = Join-Path $BackupDir "testdata-$datestamp.db"
try {
Copy-Item -Path $SourceDb -Destination $backupFile -Force
$sizeKb = [math]::Round((Get-Item $backupFile).Length / 1024, 1)
Write-Log "[OK] Backup created: $backupFile ($sizeKb KB)"
} catch {
Write-Log "[ERROR] Backup failed: $_"
exit 1
}
# Prune old backups beyond retention period
$cutoff = (Get-Date).AddDays(-$Retention)
$oldBackups = Get-ChildItem -Path $BackupDir -Filter 'testdata-*.db' |
Where-Object { $_.LastWriteTime -lt $cutoff }
foreach ($old in $oldBackups) {
try {
Remove-Item -Path $old.FullName -Force
Write-Log "[OK] Deleted old backup: $($old.Name)"
} catch {
Write-Log "[WARNING] Could not delete old backup: $($old.Name) - $_"
}
}
$remaining = (Get-ChildItem -Path $BackupDir -Filter 'testdata-*.db').Count
Write-Log "[INFO] Backup complete. $remaining backup(s) on disk."

View File

@@ -0,0 +1,134 @@
# deploy.ps1
# Deploys the fixed TestDataDB application to AD2 (192.168.0.6).
#
# Copies files via SCP, installs dependencies, sets up the Windows service,
# and installs the backup scheduled task.
#
# Must be run from the directory containing the fixed files.
$ErrorActionPreference = 'Stop'
$RemoteHost = '192.168.0.6'
$RemoteUser = 'INTRANET\sysadmin'
$RemotePass = 'Paper123!@#'
$RemotePath = 'C:\Shares\testdatadb'
$SshExe = 'C:\Windows\System32\OpenSSH\ssh.exe'
$ScpExe = 'C:\Windows\System32\OpenSSH\scp.exe'
$LocalDir = $PSScriptRoot
# Credentials for SSH - set up sshpass-equivalent via environment
# Note: For automated deployment, the SSH key should be pre-configured.
# This script uses password-based auth via the ssh client.
$SshTarget = "${RemoteUser}@${RemoteHost}"
function Invoke-RemoteCommand {
param([string]$Command)
Write-Host "[INFO] Remote: $Command"
& $SshExe $SshTarget $Command
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] Remote command failed with exit code $LASTEXITCODE"
exit 1
}
}
function Copy-ToRemote {
param([string]$LocalFile, [string]$RemoteDir)
$remoteDest = "${SshTarget}:`"${RemoteDir}`""
Write-Host "[INFO] Copying $LocalFile -> $RemoteDir"
& $ScpExe $LocalFile $remoteDest
if ($LASTEXITCODE -ne 0) {
Write-Host "[ERROR] SCP failed for $LocalFile"
exit 1
}
}
Write-Host '========================================'
Write-Host 'TestDataDB Deployment to AD2'
Write-Host '========================================'
Write-Host ''
# -----------------------------------------------------------------------
# Step 1: Ensure remote directories exist
# -----------------------------------------------------------------------
Write-Host '[STEP 1] Creating remote directories...'
Invoke-RemoteCommand "if not exist `"${RemotePath}\routes`" mkdir `"${RemotePath}\routes`""
Invoke-RemoteCommand "if not exist `"${RemotePath}\logs`" mkdir `"${RemotePath}\logs`""
Invoke-RemoteCommand "if not exist `"${RemotePath}\backups`" mkdir `"${RemotePath}\backups`""
# -----------------------------------------------------------------------
# Step 2: Stop existing node process / service
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 2] Stopping existing processes...'
# Try stopping the service first (may not exist yet)
& $SshExe $SshTarget "net stop TestDataDB 2>nul & echo Service stop attempted"
# Kill any lingering node processes running server.js
& $SshExe $SshTarget "taskkill /F /FI `"IMAGENAME eq node.exe`" 2>nul & echo Process kill attempted"
Write-Host '[OK] Existing processes stopped.'
# -----------------------------------------------------------------------
# Step 3: Copy files to remote
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 3] Copying files to AD2...'
$filesToCopy = @(
@{ Local = "$LocalDir\server.js"; Remote = $RemotePath },
@{ Local = "$LocalDir\package.json"; Remote = $RemotePath },
@{ Local = "$LocalDir\install-service.js"; Remote = $RemotePath },
@{ Local = "$LocalDir\uninstall-service.js"; Remote = $RemotePath },
@{ Local = "$LocalDir\backup-db.ps1"; Remote = $RemotePath },
@{ Local = "$LocalDir\install-backup-task.ps1"; Remote = $RemotePath },
@{ Local = "$LocalDir\routes\api.js"; Remote = "$RemotePath\routes" }
)
foreach ($file in $filesToCopy) {
Copy-ToRemote -LocalFile $file.Local -RemoteDir $file.Remote
}
Write-Host '[OK] All files copied.'
# -----------------------------------------------------------------------
# Step 4: Install npm dependencies
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 4] Installing npm dependencies...'
Invoke-RemoteCommand "cd /d `"${RemotePath}`" && npm install --production"
Write-Host '[OK] Dependencies installed.'
# -----------------------------------------------------------------------
# Step 5: Uninstall old service (if present) and install new one
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 5] Installing Windows service...'
# Uninstall first to ensure clean state
& $SshExe $SshTarget "cd /d `"${RemotePath}`" && node uninstall-service.js 2>nul"
Start-Sleep -Seconds 3
Invoke-RemoteCommand "cd /d `"${RemotePath}`" && node install-service.js"
Write-Host '[OK] Windows service installed.'
# -----------------------------------------------------------------------
# Step 6: Install backup scheduled task
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 6] Installing backup scheduled task...'
Invoke-RemoteCommand "powershell -NoProfile -ExecutionPolicy Bypass -File `"${RemotePath}\install-backup-task.ps1`""
Write-Host '[OK] Backup task installed.'
# -----------------------------------------------------------------------
# Step 7: Verify service is running
# -----------------------------------------------------------------------
Write-Host ''
Write-Host '[STEP 7] Verifying service status...'
Start-Sleep -Seconds 5
& $SshExe $SshTarget "sc query TestDataDB"
Write-Host ''
Write-Host '========================================'
Write-Host '[OK] Deployment complete.'
Write-Host "[INFO] Service: TestDataDB on $RemoteHost"
Write-Host "[INFO] URL: http://${RemoteHost}:3000"
Write-Host "[INFO] Logs: ${RemotePath}\logs\"
Write-Host "[INFO] Backups: ${RemotePath}\backups\ (daily at 2 AM)"
Write-Host '========================================'

View File

@@ -0,0 +1,55 @@
# install-backup-task.ps1
# Creates a Windows Scheduled Task to run backup-db.ps1 daily at 2:00 AM.
# Must be run as Administrator.
$ErrorActionPreference = 'Stop'
$TaskName = 'TestDataDB-Backup'
$ScriptPath = 'C:\Shares\testdatadb\backup-db.ps1'
# Check for admin privileges
$principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
Write-Host '[ERROR] This script must be run as Administrator.'
exit 1
}
# Remove existing task if present
$existing = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
if ($existing) {
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
Write-Host "[INFO] Removed existing task: $TaskName"
}
# Build task components
$action = New-ScheduledTaskAction `
-Execute 'powershell.exe' `
-Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
$trigger = New-ScheduledTaskTrigger -Daily -At '2:00AM'
$settings = New-ScheduledTaskSettingsSet `
-AllowStartIfOnBatteries `
-DontStopIfGoingOnBatteries `
-StartWhenAvailable `
-RunOnlyIfNetworkAvailable:$false `
-ExecutionTimeLimit (New-TimeSpan -Minutes 30)
$taskPrincipal = New-ScheduledTaskPrincipal `
-UserId 'SYSTEM' `
-LogonType ServiceAccount `
-RunLevel Highest
# Register task
Register-ScheduledTask `
-TaskName $TaskName `
-Action $action `
-Trigger $trigger `
-Settings $settings `
-Principal $taskPrincipal `
-Description 'Daily backup of TestDataDB SQLite database with 7-day retention' |
Out-Null
Write-Host "[OK] Scheduled task '$TaskName' created."
Write-Host "[INFO] Runs daily at 2:00 AM as SYSTEM."
Write-Host "[INFO] Script: $ScriptPath"

View File

@@ -0,0 +1,51 @@
/**
* Install TestDataDB as a Windows Service
*
* Uses node-windows to register the server as a persistent service
* with automatic restart on crash.
*
* Run: node install-service.js
*/
const path = require('path');
const Service = require('node-windows').Service;
const svc = new Service({
name: 'TestDataDB',
description: 'Dataforth Test Data Database Server',
script: path.join(__dirname, 'server.js'),
nodeOptions: [],
workingDirectory: __dirname,
allowServiceLogon: true,
// Restart configuration: max 3 restarts with 5-second delay
maxRestarts: 3,
maxRetries: 3,
wait: 5,
grow: 0.5
});
// Set log directory
svc.logpath = path.join('C:', 'Shares', 'testdatadb', 'logs');
svc.on('install', () => {
console.log('[OK] TestDataDB service installed successfully.');
console.log('[INFO] Starting service...');
svc.start();
});
svc.on('start', () => {
console.log('[OK] TestDataDB service started.');
});
svc.on('alreadyinstalled', () => {
console.log('[WARNING] TestDataDB service is already installed.');
console.log('[INFO] To reinstall, run uninstall-service.js first.');
});
svc.on('error', (err) => {
console.error('[ERROR] Service installation failed:', err);
});
console.log('[INFO] Installing TestDataDB as a Windows service...');
console.log('[INFO] Log directory: C:\\Shares\\testdatadb\\logs\\');
svc.install();

View File

@@ -0,0 +1,18 @@
{
"name": "testdatadb",
"version": "1.1.0",
"description": "Test data database and search interface",
"main": "server.js",
"scripts": {
"start": "node server.js",
"import": "node database/import.js",
"install-service": "node install-service.js",
"uninstall-service": "node uninstall-service.js"
},
"dependencies": {
"better-sqlite3": "^9.4.3",
"cors": "^2.8.5",
"express": "^4.18.2",
"node-windows": "^1.0.0-beta.8"
}
}

View File

@@ -0,0 +1,363 @@
/**
* API Routes for Test Data Database
*
* Fixed version - uses a single persistent database connection instead of
* opening and closing on every request. WAL journal mode enabled for
* concurrent read support. Limit parameter capped at 1000.
*/
const express = require('express');
const path = require('path');
const Database = require('better-sqlite3');
const { generateDatasheet } = require('../templates/datasheet');
const router = express.Router();
// ---------------------------------------------------------------------------
// Singleton database connection - opened once at module load
// ---------------------------------------------------------------------------
const DB_PATH = path.join(__dirname, '..', 'database', 'testdata.db');
const db = new Database(DB_PATH, { readonly: false });
db.pragma('journal_mode = WAL');
db.pragma('busy_timeout = 5000');
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const MAX_LIMIT = 1000;
function clampLimit(value) {
const parsed = parseInt(value, 10);
if (isNaN(parsed) || parsed < 1) return 100;
return Math.min(parsed, MAX_LIMIT);
}
function clampOffset(value) {
const parsed = parseInt(value, 10);
if (isNaN(parsed) || parsed < 0) return 0;
return parsed;
}
// ---------------------------------------------------------------------------
// GET /api/search
// Search test records
// Query params: serial, model, from, to, result, q, station, logtype, limit, offset
// ---------------------------------------------------------------------------
router.get('/search', (req, res) => {
try {
const { serial, model, from, to, result, q, station, logtype } = req.query;
const limit = clampLimit(req.query.limit || 100);
const offset = clampOffset(req.query.offset || 0);
let sql = 'SELECT * FROM test_records WHERE 1=1';
const params = [];
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
}
if (q) {
// Full-text search - rebuild query with FTS
sql = `SELECT test_records.* FROM test_records
JOIN test_records_fts ON test_records.id = test_records_fts.rowid
WHERE test_records_fts MATCH ?`;
params.length = 0;
params.push(q);
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
}
}
sql += ' ORDER BY test_date DESC, serial_number';
sql += ' LIMIT ? OFFSET ?';
params.push(limit, offset);
const records = db.prepare(sql).all(...params);
// Get total count
let countSql = sql.replace(/SELECT .* FROM/, 'SELECT COUNT(*) as count FROM')
.replace(/ORDER BY.*$/, '');
countSql = countSql.replace(/LIMIT \? OFFSET \?/, '');
const countParams = params.slice(0, -2);
const total = db.prepare(countSql).get(...countParams);
res.json({
records,
total: total?.count || records.length,
limit,
offset
});
} catch (err) {
console.error(`[${new Date().toISOString()}] [SEARCH ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/record/:id
// Get single record by ID
// ---------------------------------------------------------------------------
router.get('/record/:id', (req, res) => {
try {
const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id);
if (!record) {
return res.status(404).json({ error: 'Record not found' });
}
res.json(record);
} catch (err) {
console.error(`[${new Date().toISOString()}] [RECORD ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/datasheet/:id
// Generate datasheet for a record
// Query params: format (html, txt)
// ---------------------------------------------------------------------------
router.get('/datasheet/:id', (req, res) => {
try {
const record = db.prepare('SELECT * FROM test_records WHERE id = ?').get(req.params.id);
if (!record) {
return res.status(404).json({ error: 'Record not found' });
}
const format = req.query.format || 'html';
const datasheet = generateDatasheet(record, format);
if (format === 'html') {
res.type('html').send(datasheet);
} else {
res.type('text/plain').send(datasheet);
}
} catch (err) {
console.error(`[${new Date().toISOString()}] [DATASHEET ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/stats
// Get database statistics
// ---------------------------------------------------------------------------
router.get('/stats', (req, res) => {
try {
const stats = {
total_records: db.prepare('SELECT COUNT(*) as count FROM test_records').get().count,
by_log_type: db.prepare(`
SELECT log_type, COUNT(*) as count
FROM test_records
GROUP BY log_type
ORDER BY count DESC
`).all(),
by_result: db.prepare(`
SELECT overall_result, COUNT(*) as count
FROM test_records
GROUP BY overall_result
`).all(),
by_station: db.prepare(`
SELECT test_station, COUNT(*) as count
FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
GROUP BY test_station
ORDER BY test_station
`).all(),
date_range: db.prepare(`
SELECT MIN(test_date) as oldest, MAX(test_date) as newest
FROM test_records
`).get(),
recent_serials: db.prepare(`
SELECT DISTINCT serial_number, model_number, test_date
FROM test_records
ORDER BY test_date DESC
LIMIT 10
`).all()
};
res.json(stats);
} catch (err) {
console.error(`[${new Date().toISOString()}] [STATS ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/filters
// Get available filter options (test stations, log types, models)
// ---------------------------------------------------------------------------
router.get('/filters', (req, res) => {
try {
const filters = {
stations: db.prepare(`
SELECT DISTINCT test_station
FROM test_records
WHERE test_station IS NOT NULL AND test_station != ''
ORDER BY test_station
`).all().map(r => r.test_station),
log_types: db.prepare(`
SELECT DISTINCT log_type
FROM test_records
ORDER BY log_type
`).all().map(r => r.log_type),
models: db.prepare(`
SELECT DISTINCT model_number, COUNT(*) as count
FROM test_records
GROUP BY model_number
ORDER BY count DESC
LIMIT 500
`).all()
};
res.json(filters);
} catch (err) {
console.error(`[${new Date().toISOString()}] [FILTERS ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// GET /api/export
// Export search results as CSV
// ---------------------------------------------------------------------------
router.get('/export', (req, res) => {
try {
const { serial, model, from, to, result, station, logtype } = req.query;
let sql = 'SELECT * FROM test_records WHERE 1=1';
const params = [];
if (serial) {
sql += ' AND serial_number LIKE ?';
params.push(serial.includes('%') ? serial : `%${serial}%`);
}
if (model) {
sql += ' AND model_number LIKE ?';
params.push(model.includes('%') ? model : `%${model}%`);
}
if (from) {
sql += ' AND test_date >= ?';
params.push(from);
}
if (to) {
sql += ' AND test_date <= ?';
params.push(to);
}
if (result) {
sql += ' AND overall_result = ?';
params.push(result.toUpperCase());
}
if (station) {
sql += ' AND test_station = ?';
params.push(station);
}
if (logtype) {
sql += ' AND log_type = ?';
params.push(logtype);
}
sql += ' ORDER BY test_date DESC, serial_number LIMIT 10000';
const records = db.prepare(sql).all(...params);
// Generate CSV
const headers = ['id', 'log_type', 'model_number', 'serial_number', 'test_date', 'test_station', 'overall_result', 'source_file'];
let csv = headers.join(',') + '\n';
for (const record of records) {
const row = headers.map(h => {
const val = record[h] || '';
return `"${String(val).replace(/"/g, '""')}"`;
});
csv += row.join(',') + '\n';
}
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=test_records.csv');
res.send(csv);
} catch (err) {
console.error(`[${new Date().toISOString()}] [EXPORT ERROR] ${err.message}`);
res.status(500).json({ error: err.message });
}
});
// ---------------------------------------------------------------------------
// Cleanup function for graceful shutdown
// ---------------------------------------------------------------------------
function cleanup() {
try {
db.close();
} catch (err) {
console.error(`[${new Date().toISOString()}] [CLEANUP ERROR] ${err.message}`);
}
}
module.exports = router;
module.exports.cleanup = cleanup;

View File

@@ -0,0 +1,103 @@
/**
* Test Data Database Server
* Express.js server with search API and web interface
*
* Fixed version - singleton DB connection, crash resilience,
* graceful shutdown, request logging.
*/
const express = require('express');
const cors = require('cors');
const path = require('path');
const apiRoutes = require('./routes/api');
const { cleanup } = require('./routes/api');
const app = express();
const PORT = process.env.PORT || 3000;
const HOST = '0.0.0.0';
// ---------------------------------------------------------------------------
// Crash resilience - log and continue rather than dying
// ---------------------------------------------------------------------------
process.on('uncaughtException', (err) => {
console.error(`[${new Date().toISOString()}] [UNCAUGHT EXCEPTION] ${err.stack || err.message}`);
});
process.on('unhandledRejection', (reason) => {
console.error(`[${new Date().toISOString()}] [UNHANDLED REJECTION] ${reason}`);
});
// ---------------------------------------------------------------------------
// Middleware
// ---------------------------------------------------------------------------
app.use(cors());
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
// Request logging
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
console.log(
`[${new Date().toISOString()}] ${req.method} ${req.originalUrl} ${res.statusCode} ${duration}ms`
);
});
next();
});
// ---------------------------------------------------------------------------
// Routes
// ---------------------------------------------------------------------------
app.use('/api', apiRoutes);
// Serve index.html for root
app.get('/', (req, res) => {
res.sendFile(path.join(__dirname, 'public', 'index.html'));
});
// ---------------------------------------------------------------------------
// Start server
// ---------------------------------------------------------------------------
const server = app.listen(PORT, HOST, () => {
console.log(`\n========================================`);
console.log(`Test Data Database Server`);
console.log(`========================================`);
console.log(`Server running on all interfaces (${HOST}:${PORT})`);
console.log(`Local: http://localhost:${PORT}`);
console.log(`LAN: http://192.168.0.6:${PORT}`);
console.log(`API endpoints:`);
console.log(` GET /api/search?serial=...&model=...`);
console.log(` GET /api/record/:id`);
console.log(` GET /api/datasheet/:id`);
console.log(` GET /api/stats`);
console.log(` GET /api/filters`);
console.log(` GET /api/export?format=csv&...`);
console.log(`========================================\n`);
});
// ---------------------------------------------------------------------------
// Graceful shutdown
// ---------------------------------------------------------------------------
function shutdown(signal) {
console.log(`\n[${new Date().toISOString()}] Received ${signal}. Shutting down gracefully...`);
server.close(() => {
console.log(`[${new Date().toISOString()}] HTTP server closed.`);
cleanup();
console.log(`[${new Date().toISOString()}] Database connection closed. Goodbye.`);
process.exit(0);
});
// Force exit after 10 seconds if graceful shutdown stalls
setTimeout(() => {
console.error(`[${new Date().toISOString()}] Forced shutdown after timeout.`);
cleanup();
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
module.exports = app;

View File

@@ -0,0 +1,24 @@
/**
* Uninstall TestDataDB Windows Service
*
* Run: node uninstall-service.js
*/
const path = require('path');
const Service = require('node-windows').Service;
const svc = new Service({
name: 'TestDataDB',
script: path.join(__dirname, 'server.js')
});
svc.on('uninstall', () => {
console.log('[OK] TestDataDB service uninstalled successfully.');
});
svc.on('error', (err) => {
console.error('[ERROR] Service uninstall failed:', err);
});
console.log('[INFO] Uninstalling TestDataDB Windows service...');
svc.uninstall();

View File

@@ -1 +1 @@
VITE_API_URL=/msp-api
VITE_API_URL=/quote/api

View File

@@ -25,26 +25,23 @@ require_once __DIR__ . '/../services/syncro_service.php';
*/
function check_admin_auth(): void
{
$header = $_SERVER['HTTP_AUTHORIZATION']
?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
?? '';
// suPHP strips the Authorization header, so accept X-Api-Key as primary
$token = $_SERVER['HTTP_X_API_KEY'] ?? '';
// Apache CGI/suPHP may strip Authorization header; check env var fallback
if (empty($header) && !empty(getenv('HTTP_AUTHORIZATION'))) {
$header = getenv('HTTP_AUTHORIZATION');
// Fallback: try Authorization: Bearer {key} (works with PHP-FPM)
if (empty($token)) {
$header = $_SERVER['HTTP_AUTHORIZATION']
?? $_SERVER['REDIRECT_HTTP_AUTHORIZATION']
?? '';
if (!empty($header) && strpos($header, 'Bearer ') === 0) {
$token = substr($header, 7);
}
}
if (empty($header)) {
error_response('Authorization header required', 401);
if (empty($token)) {
error_response('API key required. Send X-Api-Key header.', 401);
}
// Extract bearer token
if (strpos($header, 'Bearer ') !== 0) {
error_response('Invalid authorization format. Expected: Bearer {api_key}', 401);
}
$token = substr($header, 7);
if (ADMIN_API_KEY === 'CHANGE_ME_PLACEHOLDER') {
app_log('WARNING', '[WARNING] Admin API key is not configured (still placeholder)');
error_response('Admin API key not configured on server', 500);

28
temp/ad2-diag.ps1 Normal file
View File

@@ -0,0 +1,28 @@
# Diagnostic script for TestDataDB on AD2
Write-Output "=== Node Process ==="
Get-CimInstance Win32_Process -Filter "Name='node.exe'" | Select-Object ProcessId, CommandLine | Format-List
Write-Output "=== HTTP Test ==="
try {
$r = Invoke-WebRequest -Uri "http://localhost:3000/" -UseBasicParsing -TimeoutSec 10
Write-Output "Root page status: $($r.StatusCode)"
Write-Output "Content length: $($r.Content.Length)"
Write-Output "First 200 chars: $($r.Content.Substring(0, [Math]::Min(200, $r.Content.Length)))"
} catch {
Write-Output "Root page ERROR: $($_.Exception.Message)"
}
Write-Output "`n=== API Test ==="
try {
$r = Invoke-WebRequest -Uri "http://localhost:3000/api/stats" -UseBasicParsing -TimeoutSec 10
Write-Output "API status: $($r.StatusCode)"
Write-Output "First 200 chars: $($r.Content.Substring(0, [Math]::Min(200, $r.Content.Length)))"
} catch {
Write-Output "API ERROR: $($_.Exception.Message)"
}
Write-Output "`n=== Service Log Files ==="
Get-ChildItem "C:\Shares\testdatadb\logs\" -ErrorAction SilentlyContinue | Format-Table Name, Length, LastWriteTime
Write-Output "`n=== Recent Event Log ==="
Get-EventLog -LogName Application -Newest 5 -Source "*node*" -ErrorAction SilentlyContinue | Format-List

51
temp/bgb-lesley-check.ps1 Normal file
View File

@@ -0,0 +1,51 @@
# Check Lesley's email activity since disable
$ErrorActionPreference = "Stop"
$lesleyUPN = "lesley@bgbuildersllc.com"
Import-Module ExchangeOnlineManagement
Connect-ExchangeOnline -UserPrincipalName "sysadmin@bgbuildersllc.com" -ShowBanner:$false
$startDate = (Get-Date).AddDays(-3)
$endDate = Get-Date
Write-Output "=== MAILBOX STATUS ==="
$mbx = Get-Mailbox -Identity $lesleyUPN
$stats = Get-MailboxStatistics -Identity $lesleyUPN
Write-Output "Type: $($mbx.RecipientTypeDetails)"
Write-Output "LitigationHold: $($mbx.LitigationHoldEnabled)"
Write-Output "ItemCount: $($stats.ItemCount)"
Write-Output "TotalSize: $($stats.TotalItemSize)"
Write-Output "`n=== SENT MESSAGES (last 3 days) ==="
$sent = Get-MessageTraceV2 -SenderAddress $lesleyUPN -StartDate $startDate -EndDate $endDate
if ($sent) {
$sent | Format-Table Received,RecipientAddress,Subject -AutoSize
} else {
Write-Output "None found"
}
Write-Output "`n=== RECEIVED MESSAGES (last 3 days) ==="
$recv = Get-MessageTraceV2 -RecipientAddress $lesleyUPN -StartDate $startDate -EndDate $endDate
if ($recv) {
$recv | Select-Object -First 20 | Format-Table Received,SenderAddress,Subject -AutoSize
} else {
Write-Output "None found"
}
Write-Output "`n=== INBOX RULES ==="
$rules = Get-InboxRule -Mailbox $lesleyUPN
if ($rules) {
$rules | Format-Table Name,Enabled,Description -AutoSize
} else {
Write-Output "No inbox rules"
}
Write-Output "`n=== FORWARDING CONFIG ==="
Write-Output "ForwardingAddress: $($mbx.ForwardingAddress)"
Write-Output "ForwardingSmtpAddress: $($mbx.ForwardingSmtpAddress)"
Write-Output "DeliverToMailboxAndForward: $($mbx.DeliverToMailboxAndForward)"
Write-Output "`n=== FOLDER ITEM COUNTS ==="
Get-MailboxFolderStatistics -Identity $lesleyUPN | Where-Object { $_.ItemsInFolder -gt 0 } | Sort-Object ItemsInFolder -Descending | Select-Object -First 15 | Format-Table Name,FolderType,ItemsInFolder,FolderSize -AutoSize
Disconnect-ExchangeOnline -Confirm:$false

View File

@@ -0,0 +1,52 @@
# Update MFA phone number for Lesley Roth @ BG Builders
$ErrorActionPreference = "Stop"
$lesleyUPN = "lesley@bgbuildersllc.com"
$newPhone = "+1 4804954511"
$tenantId = "ededa4fb-f6eb-4398-851d-5eb3e11fab27"
Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.Users
Connect-MgGraph -TenantId $tenantId -Scopes 'UserAuthenticationMethod.ReadWrite.All','User.ReadWrite.All' -NoWelcome
Write-Output "=== Current Auth Methods for Lesley ==="
$methods = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods"
if ($methods.value.Count -gt 0) {
foreach ($m in $methods.value) {
Write-Output " ID: $($m.id) | Type: $($m.phoneType) | Number: $($m.phoneNumber)"
}
} else {
Write-Output " No phone methods registered"
}
Write-Output "`n=== Updating MFA Phone ==="
# Phone method ID for mobile is always "3179e48a-750b-4051-897c-87b9720928f7"
$mobileMethodId = "3179e48a-750b-4051-897c-87b9720928f7"
try {
# Try to update existing mobile phone method
Invoke-MgGraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods/$mobileMethodId" -Body @{
phoneNumber = $newPhone
phoneType = "mobile"
}
Write-Output "[OK] Mobile phone updated to $newPhone"
} catch {
Write-Output "[INFO] PUT failed, trying POST to create new method..."
try {
Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods" -Body @{
phoneNumber = $newPhone
phoneType = "mobile"
}
Write-Output "[OK] Mobile phone created: $newPhone"
} catch {
Write-Output "[ERROR] Failed: $_"
}
}
Write-Output "`n=== Verify Updated Methods ==="
$methods = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods"
foreach ($m in $methods.value) {
Write-Output " ID: $($m.id) | Type: $($m.phoneType) | Number: $($m.phoneNumber)"
}
Disconnect-MgGraph

View File

@@ -0,0 +1,26 @@
# Update MFA phone number for Lesley Roth @ BG Builders
$ErrorActionPreference = "Stop"
$lesleyUPN = "lesley@bgbuildersllc.com"
$newPhone = "+1 4804954511"
$tenantId = "ededa4fb-f6eb-4398-851d-5eb3e11fab27"
Import-Module Microsoft.Graph.Authentication
Connect-MgGraph -TenantId $tenantId -Scopes 'UserAuthenticationMethod.ReadWrite.All' -NoWelcome
$mobileMethodId = "3179e48a-750b-4051-897c-87b9720928f7"
Write-Output "Current: +1 4802299138"
Write-Output "Changing to: $newPhone"
$body = @{ phoneNumber = $newPhone; phoneType = "mobile" } | ConvertTo-Json
Invoke-MgGraphRequest -Method PUT -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods/$mobileMethodId" -Body $body -ContentType "application/json"
Write-Output "[OK] Phone updated"
# Verify
$methods = Invoke-MgGraphRequest -Method GET -Uri "https://graph.microsoft.com/v1.0/users/$lesleyUPN/authentication/phoneMethods"
foreach ($m in $methods.value) {
Write-Output "Verified: $($m.phoneType) = $($m.phoneNumber)"
}
Disconnect-MgGraph

View File

@@ -0,0 +1,52 @@
"""Generate backup codes for office@lonestarelectrical.net so Kyla can bypass 2FA enrollment block"""
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.security',
]
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'office@lonestarelectrical.net'
# Check current 2SV status
print(f"=== {user_email} 2SV Status ===")
user = service.users().get(userKey=user_email).execute()
print(f"2SV Enrolled: {user.get('isEnrolledIn2Sv', False)}")
print(f"2SV Enforced: {user.get('isEnforcedIn2Sv', False)}")
# Generate backup verification codes
print(f"\n=== Generating Backup Codes ===")
try:
codes = service.verificationCodes().generate(userKey=user_email).execute()
print("[OK] Backup codes generated")
except Exception as e:
print(f"[INFO] Generate returned: {e}")
# List the codes
try:
result = service.verificationCodes().list(userKey=user_email).execute()
backup_codes = result.get('items', [])
if backup_codes:
print(f"\nBackup codes for Kyla to use at login:")
for code in backup_codes:
status = code.get('etag', '')
print(f" {code.get('verificationCode', 'N/A')}")
print(f"\nInstructions for Kyla:")
print(f" 1. Go to https://accounts.google.com")
print(f" 2. Enter email: {user_email}")
print(f" 3. Enter the temp password we set")
print(f" 4. When prompted for 2FA, click 'Try another way'")
print(f" 5. Select 'Enter a backup code'")
print(f" 6. Use one of the codes above")
print(f" 7. Once logged in, go to Security > 2-Step Verification to set up her phone")
else:
print("[WARNING] No codes returned")
except Exception as e:
print(f"[ERROR] Could not list codes: {e}")

View File

@@ -0,0 +1,60 @@
"""Reset password for office@lonestarelectrical.net so Kyla can login and set up MFA"""
import secrets
import string
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.security',
]
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'office@lonestarelectrical.net'
# Check current user status
print(f"=== Checking {user_email} ===")
try:
user = service.users().get(userKey=user_email).execute()
print(f"Name: {user.get('name', {}).get('fullName', 'N/A')}")
print(f"Suspended: {user.get('suspended', 'N/A')}")
print(f"Archived: {user.get('archived', 'N/A')}")
print(f"2FA Enrolled: {user.get('isEnrolledIn2Sv', 'N/A')}")
print(f"2FA Enforced: {user.get('isEnforcedIn2Sv', 'N/A')}")
print(f"Last Login: {user.get('lastLoginTime', 'N/A')}")
print(f"Creation: {user.get('creationTime', 'N/A')}")
except Exception as e:
print(f"[ERROR] Could not get user: {e}")
exit(1)
# Generate a temp password
alphabet = string.ascii_letters + string.digits + "!@#$"
temp_pass = ''.join(secrets.choice(alphabet) for _ in range(16))
# Reset password, require change on next login
print(f"\n=== Resetting password ===")
try:
service.users().update(
userKey=user_email,
body={
'password': temp_pass,
'changePasswordAtNextLogin': True,
'suspended': False,
}
).execute()
print(f"[OK] Password reset successful")
print(f"[OK] Account unsuspended (if it was)")
print(f"[OK] Must change password on first login")
print(f"\nTemporary password: {temp_pass}")
print(f"\nGive Kyla:")
print(f" Email: {user_email}")
print(f" Password: {temp_pass}")
print(f" URL: https://accounts.google.com")
print(f" She will be prompted to change password and set up MFA")
except Exception as e:
print(f"[ERROR] Password reset failed: {e}")

View File

@@ -0,0 +1,25 @@
"""Reset password for office@lonestarelectrical.net - attempt 2, no force change"""
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = ['https://www.googleapis.com/auth/admin.directory.user']
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'office@lonestarelectrical.net'
new_pass = 'LoneStar2026!!'
service.users().update(
userKey=user_email,
body={
'password': new_pass,
'changePasswordAtNextLogin': False,
}
).execute()
print(f"[OK] Password reset for {user_email}")
print(f"Password: {new_pass}")

View File

@@ -0,0 +1,41 @@
"""Reset password and generate backup codes for russ@lonestarelectrical.net"""
from google.oauth2 import service_account
from googleapiclient.discovery import build
SCOPES = [
'https://www.googleapis.com/auth/admin.directory.user',
'https://www.googleapis.com/auth/admin.directory.user.security',
]
creds = service_account.Credentials.from_service_account_file(
'temp/acg-msp-access-8f72339997e5.json', scopes=SCOPES
)
delegated = creds.with_subject('sysadmin@lonestarelectrical.net')
service = build('admin', 'directory_v1', credentials=delegated)
user_email = 'russ@lonestarelectrical.net'
# Check user
print(f"=== {user_email} ===")
user = service.users().get(userKey=user_email).execute()
print(f"Name: {user.get('name', {}).get('fullName', 'N/A')}")
print(f"2SV Enrolled: {user.get('isEnrolledIn2Sv', False)}")
print(f"2SV Enforced: {user.get('isEnforcedIn2Sv', False)}")
print(f"Last Login: {user.get('lastLoginTime', 'N/A')}")
# Reset password
new_pass = 'LoneStar2026!!'
service.users().update(
userKey=user_email,
body={'password': new_pass, 'changePasswordAtNextLogin': False, 'suspended': False}
).execute()
print(f"\n[OK] Password reset: {new_pass}")
# Generate backup codes
service.verificationCodes().generate(userKey=user_email).execute()
result = service.verificationCodes().list(userKey=user_email).execute()
codes = result.get('items', [])
if codes:
print(f"\nBackup codes:")
for c in codes:
print(f" {c.get('verificationCode')}")

19
temp/test-ad2-web.ps1 Normal file
View File

@@ -0,0 +1,19 @@
$SshExe = 'C:\Windows\System32\OpenSSH\ssh.exe'
$SshTarget = 'INTRANET\sysadmin@192.168.0.6'
# Create a test script on AD2
$testScript = @'
try {
$r = Invoke-WebRequest -Uri "http://localhost:3000/api/stats" -UseBasicParsing -TimeoutSec 5
Write-Output "STATUS: $($r.StatusCode)"
Write-Output "CONTENT: $($r.Content.Substring(0, [Math]::Min(200, $r.Content.Length)))"
} catch {
Write-Output "ERROR: $($_.Exception.Message)"
}
'@
# Write test script to AD2
$testScript | & $SshExe $SshTarget 'powershell -Command "Set-Content -Path C:\Shares\testdatadb\test-web.ps1 -Value (Get-Content -Raw -Path -)"'
# Actually, simpler - just run inline
& $SshExe $SshTarget 'powershell -NoProfile -ExecutionPolicy Bypass -Command "try { $r = Invoke-WebRequest -Uri http://localhost:3000/ -UseBasicParsing -TimeoutSec 5; Write-Output STATUS:$($r.StatusCode) } catch { Write-Output ERROR:$($_.Exception.Message) }"'

3
temp/test-minimal.ps1 Normal file
View File

@@ -0,0 +1,3 @@
# Minimal test - just echo to NAS
$r = & "C:\Program Files\OpenSSH\ssh.exe" -i C:\Users\sysadmin\.ssh\id_ed25519 -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new root@192.168.0.9 "echo MINIMAL_TEST_OK" 2>&1
Write-Host "Result: $r"

View File

@@ -0,0 +1,48 @@
# Test script to run ON AD2 - diagnoses NAS SSH hang issue
$SSH = "C:\Program Files\OpenSSH\ssh.exe"
$SSH_KEY = "C:\Users\sysadmin\.ssh\id_ed25519"
$NAS_USER = "root"
$NAS_IP = "192.168.0.9"
Write-Host "=== Step 1: Kill any hung SSH processes ==="
Get-Process ssh -ErrorAction SilentlyContinue | ForEach-Object {
Write-Host " Killing SSH PID $($_.Id)"
Stop-Process -Id $_.Id -Force
}
Get-Process powershell -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $PID } | ForEach-Object {
Write-Host " Other PowerShell PID $($_.Id) - CommandLine: $($_.CommandLine)"
}
Write-Host "`n=== Step 2: Basic SSH echo test ==="
$t1 = Get-Date
$r1 = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" "echo NAS_OK" 2>&1
$d1 = (Get-Date) - $t1
Write-Host " Result: $r1 (took $($d1.TotalSeconds)s)"
Write-Host "`n=== Step 3: find with temp file redirect (the actual fix) ==="
$t2 = Get-Date
Write-Host " Running find with output to /tmp/test-list.txt..."
# This is exactly what Get-NASFileList does - output goes to file on NAS, stdout discarded
& $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}" "find /data/test/TS-*/LOGS -name '*.DAT' -type f -mmin -1440 > /tmp/test-list.txt 2>/dev/null" *> $null
$d2 = (Get-Date) - $t2
Write-Host " SSH returned in $($d2.TotalSeconds)s"
Write-Host "`n=== Step 4: Count files found ==="
$r3 = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 "${NAS_USER}@${NAS_IP}" "wc -l /tmp/test-list.txt; head -3 /tmp/test-list.txt" 2>&1
foreach ($line in $r3) { Write-Host " $line" }
Write-Host "`n=== Step 5: Pull file list via SCP ==="
$localTemp = "$env:TEMP\test-nas-filelist.txt"
& "C:\Program Files\OpenSSH\scp.exe" -O -i $SSH_KEY -o StrictHostKeyChecking=accept-new "${NAS_USER}@${NAS_IP}:/tmp/test-list.txt" "$localTemp" *> $null
if (Test-Path $localTemp) {
$lines = Get-Content $localTemp | Where-Object { $_.Trim() -ne '' }
Write-Host " Downloaded $($lines.Count) file paths"
Remove-Item $localTemp -ErrorAction SilentlyContinue
} else {
Write-Host " ERROR: SCP failed to download file"
}
Write-Host "`n=== Step 6: Cleanup ==="
& $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 "${NAS_USER}@${NAS_IP}" "rm -f /tmp/test-list.txt" 2>&1 | Out-Null
Write-Host "`n=== DONE ==="

32
temp/test-nas-v2.ps1 Normal file
View File

@@ -0,0 +1,32 @@
# Run ON AD2: Tests NAS SSH operations
# Deploy: scp test-nas-v2.ps1 AD2:C:\Shares\testdatadb\
# Run: powershell -NoProfile -ExecutionPolicy Bypass -File C:\Shares\testdatadb\test-nas-v2.ps1
$SSH = "C:\Program Files\OpenSSH\ssh.exe"
$SSH_KEY = "C:\Users\sysadmin\.ssh\id_ed25519"
Write-Host "=== Test A: echo ==="
$t = Get-Date
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 -o StrictHostKeyChecking=accept-new root@192.168.0.9 "echo NAS_OK" 2>&1
Write-Host " Result: $r ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test B: ls data dir ==="
$t = Get-Date
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "ls /data/test/ | head -5" 2>&1
foreach ($l in $r) { Write-Host " $l" }
Write-Host " ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test C: find with wc -l only ==="
$t = Get-Date
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "find /data/test/TS-*/LOGS -name '*.DAT' -type f -mmin -1440 2>/dev/null | wc -l" 2>&1
Write-Host " Count: $r ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test D: find to temp file ==="
$t = Get-Date
& $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "find /data/test/TS-*/LOGS -name '*.DAT' -type f -mmin -1440 > /tmp/test-list.txt 2>/dev/null" *> $null
Write-Host " ($(((Get-Date)-$t).TotalSeconds)s)"
Write-Host "=== Test E: check temp file ==="
$r = & $SSH -i $SSH_KEY -o BatchMode=yes -o ConnectTimeout=10 root@192.168.0.9 "wc -l /tmp/test-list.txt; rm -f /tmp/test-list.txt" 2>&1
Write-Host " $r"
Write-Host "=== ALL DONE ==="

15
temp/testdatadb.xml Normal file
View File

@@ -0,0 +1,15 @@
<service>
<id>testdatadb</id>
<name>TestDataDB</name>
<description>Dataforth Test Data Database Server</description>
<executable>C:\Program Files\nodejs\node.exe</executable>
<argument>C:\Shares\testdatadb\server.js</argument>
<logpath>C:\Shares\testdatadb\logs</logpath>
<logmode>rotate</logmode>
<stoptimeout>15sec</stoptimeout>
<workingdirectory>C:\Shares\testdatadb</workingdirectory>
<onfailure action="restart" delay="5 sec"/>
<onfailure action="restart" delay="10 sec"/>
<onfailure action="restart" delay="30 sec"/>
<resetfailure>1 hour</resetfailure>
</service>