Compare commits
5 Commits
1a26eb051a
...
068888202c
| Author | SHA1 | Date | |
|---|---|---|---|
| 068888202c | |||
| 6c2c693e6d | |||
| 78528d545e | |||
| 000ee3da5c | |||
| 470638ff86 |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
369
projects/dataforth-dos/d2testnas-vm/README.md
Normal file
369
projects/dataforth-dos/d2testnas-vm/README.md
Normal 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) |
|
||||
922
projects/dataforth-dos/d2testnas-vm/setup-d2testnas.sh
Normal file
922
projects/dataforth-dos/d2testnas-vm/setup-d2testnas.sh
Normal 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 "============================================================"
|
||||
@@ -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"
|
||||
366
projects/dataforth-dos/session-logs/2026-03-12-session.md
Normal file
366
projects/dataforth-dos/session-logs/2026-03-12-session.md
Normal 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
|
||||
664
projects/dataforth-dos/sync-fixes/Sync-FromNAS-rsync.ps1
Normal file
664
projects/dataforth-dos/sync-fixes/Sync-FromNAS-rsync.ps1
Normal 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
|
||||
}
|
||||
540
projects/dataforth-dos/sync-fixes/Sync-FromNAS.ps1
Normal file
540
projects/dataforth-dos/sync-fixes/Sync-FromNAS.ps1
Normal 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
|
||||
}
|
||||
302
projects/dataforth-dos/sync-fixes/deploy-sync-fixes.ps1
Normal file
302
projects/dataforth-dos/sync-fixes/deploy-sync-fixes.ps1
Normal 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
|
||||
395
projects/dataforth-dos/sync-fixes/import.js
Normal file
395
projects/dataforth-dos/sync-fixes/import.js
Normal 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 };
|
||||
61
projects/dataforth-dos/testdatadb-fix/backup-db.ps1
Normal file
61
projects/dataforth-dos/testdatadb-fix/backup-db.ps1
Normal 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."
|
||||
134
projects/dataforth-dos/testdatadb-fix/deploy.ps1
Normal file
134
projects/dataforth-dos/testdatadb-fix/deploy.ps1
Normal 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 '========================================'
|
||||
@@ -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"
|
||||
51
projects/dataforth-dos/testdatadb-fix/install-service.js
Normal file
51
projects/dataforth-dos/testdatadb-fix/install-service.js
Normal 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();
|
||||
18
projects/dataforth-dos/testdatadb-fix/package.json
Normal file
18
projects/dataforth-dos/testdatadb-fix/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
363
projects/dataforth-dos/testdatadb-fix/routes/api.js
Normal file
363
projects/dataforth-dos/testdatadb-fix/routes/api.js
Normal 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;
|
||||
103
projects/dataforth-dos/testdatadb-fix/server.js
Normal file
103
projects/dataforth-dos/testdatadb-fix/server.js
Normal 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;
|
||||
24
projects/dataforth-dos/testdatadb-fix/uninstall-service.js
Normal file
24
projects/dataforth-dos/testdatadb-fix/uninstall-service.js
Normal 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();
|
||||
@@ -1 +1 @@
|
||||
VITE_API_URL=/msp-api
|
||||
VITE_API_URL=/quote/api
|
||||
|
||||
@@ -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
28
temp/ad2-diag.ps1
Normal 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
51
temp/bgb-lesley-check.ps1
Normal 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
|
||||
52
temp/bgb-lesley-mfa-phone.ps1
Normal file
52
temp/bgb-lesley-mfa-phone.ps1
Normal 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
|
||||
26
temp/bgb-lesley-mfa-phone2.ps1
Normal file
26
temp/bgb-lesley-mfa-phone2.ps1
Normal 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
|
||||
52
temp/lonestar-kyla-2fa-fix.py
Normal file
52
temp/lonestar-kyla-2fa-fix.py
Normal 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}")
|
||||
60
temp/lonestar-kyla-reset.py
Normal file
60
temp/lonestar-kyla-reset.py
Normal 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}")
|
||||
25
temp/lonestar-kyla-reset2.py
Normal file
25
temp/lonestar-kyla-reset2.py
Normal 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}")
|
||||
41
temp/lonestar-russ-setup.py
Normal file
41
temp/lonestar-russ-setup.py
Normal 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
19
temp/test-ad2-web.ps1
Normal 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
3
temp/test-minimal.ps1
Normal 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"
|
||||
48
temp/test-nas-from-ad2.ps1
Normal file
48
temp/test-nas-from-ad2.ps1
Normal 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
32
temp/test-nas-v2.ps1
Normal 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
15
temp/testdatadb.xml
Normal 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>
|
||||
Reference in New Issue
Block a user