Built the missing piece between the test datasheet pipeline and Dataforth's new product API. End-to-end: - Pulled DFWDS (Dataforth Web Datasheet System) VB6 source from AD1\Engineering\ENGR\ATE\Test Datasheets\DFWDS to local for analysis - Decoded its filename validation: A-J prefix decodes (A=10..J=19), all- numeric WO# valid (no leading 0), anything else bad - Ported the validation + move logic to Node (dfwds-process.js) - Built bulk uploader (upload-delta.js) for Hoffman's Swagger API (POST /api/v1/TestReportDataFiles/bulk with OAuth client_credentials) Sanitized 3 prior reference scripts (fetch-server-inventory, test-scenarios, test-upload-two) to read CF_* env vars instead of hardcoded creds. Live drain results: - 897 files moved Test_Datasheets -> For_Web (all valid, no renames, no bad), DFWDS port summary in 1.1s - Pushed entire For_Web (7,061 files) to Hoffman API in 49.7s @ 142/s: Created=803 Updated=114 Unchanged=6,144 Errors=0 - Server count: 489,579 -> 490,382 (+803 net new) Also: - Added clients/dataforth/.gitignore to exclude plaintext Oauth.txt note - Added clients/instrumental-music-center/docs/2026-04-13-ticket-notes.md (ticket write-up of 2026-04-11/12/13 IMC1 RDS removal/SQL migration work) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
84 lines
3.4 KiB
Python
84 lines
3.4 KiB
Python
"""SFTP upload-delta.js + delta_to_upload.txt to AD2, then run via SSH with creds in env vars."""
|
|
import paramiko, subprocess, sys, time, yaml, os
|
|
|
|
LIMIT = 0 # 0 = all
|
|
BATCH = 100
|
|
DRY = False
|
|
START = 0
|
|
# Allow CLI overrides
|
|
for i, a in enumerate(sys.argv[1:]):
|
|
if a == '--limit': LIMIT = int(sys.argv[i+2])
|
|
if a == '--batch': BATCH = int(sys.argv[i+2])
|
|
if a == '--start': START = int(sys.argv[i+2])
|
|
if a == '--dry-run': DRY = True
|
|
|
|
ad2_pwd = yaml.safe_load(subprocess.run(['sops','-d','D:/vault/clients/dataforth/ad2.sops.yaml'],
|
|
capture_output=True, text=True, timeout=30, check=True).stdout)['credentials']['password'].replace('\\','')
|
|
api = yaml.safe_load(subprocess.run(['sops','-d','D:/vault/clients/dataforth/api-oauth.sops.yaml'],
|
|
capture_output=True, text=True, timeout=30, check=True).stdout)
|
|
|
|
REMOTE_DIR = 'C:/Users/sysadmin/Documents/dataforth-uploader'
|
|
LOCAL_DELTA = r'C:\Users\guru\AppData\Local\Temp\delta_to_upload.txt'
|
|
LOCAL_JS = r'D:\claudetools\projects\dataforth-dos\datasheet-pipeline\upload-delta.js'
|
|
|
|
c = paramiko.SSHClient(); c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
c.connect('192.168.0.6', username='sysadmin', password=ad2_pwd,
|
|
timeout=30, banner_timeout=45, look_for_keys=False, allow_agent=False)
|
|
|
|
def run(cmd, to=120):
|
|
_, o, e = c.exec_command(cmd, timeout=to)
|
|
return o.read().decode('utf-8','replace'), e.read().decode('utf-8','replace'), o.channel.recv_exit_status()
|
|
|
|
print('[1] mkdir + sftp upload')
|
|
run(f'powershell -Command "New-Item -ItemType Directory -Force -Path \\"{REMOTE_DIR}\\" | Out-Null"')
|
|
sftp = c.open_sftp()
|
|
sftp.put(LOCAL_JS, f'{REMOTE_DIR}/upload-delta.js')
|
|
sftp.put(LOCAL_DELTA, f'{REMOTE_DIR}/delta_to_upload.txt')
|
|
sftp.close()
|
|
out, _, _ = run(f'powershell -Command "Get-ChildItem \\"{REMOTE_DIR}\\" | Select Name,Length | Format-Table -AutoSize | Out-String"')
|
|
print(out.rstrip())
|
|
|
|
print('\n[2] run uploader on AD2 (env-var creds)')
|
|
flags = []
|
|
if LIMIT: flags += ['--limit', str(LIMIT)]
|
|
if BATCH: flags += ['--batch', str(BATCH)]
|
|
if START: flags += ['--start', str(START)]
|
|
if DRY: flags += ['--dry-run']
|
|
flag_str = ' '.join(flags)
|
|
|
|
# Build powershell command that sets env vars then runs node
|
|
ps_cmd = (
|
|
f'$env:CF_TOKEN_URL = "{api["endpoints"]["token-url"]}"; '
|
|
f'$env:CF_API_BASE = "{api["endpoints"]["api-base"]}"; '
|
|
f'$env:CF_CLIENT_ID = "{api["credentials"]["client-id"]}"; '
|
|
f'$env:CF_CLIENT_SECRET = "{api["credentials"]["client-secret"]}"; '
|
|
f'$env:CF_SCOPE = "{api["credentials"]["scope"]}"; '
|
|
f'cd "{REMOTE_DIR}"; '
|
|
f'& node upload-delta.js {flag_str} 2>&1'
|
|
)
|
|
import base64
|
|
enc = base64.b64encode(ps_cmd.encode('utf-16-le')).decode()
|
|
|
|
stdin, stdout, stderr = c.exec_command(f'powershell -NoProfile -EncodedCommand {enc}', timeout=7200, get_pty=False)
|
|
import threading
|
|
def reader(stream, label):
|
|
try:
|
|
for line in iter(lambda: stream.readline(), ''):
|
|
if not line: break
|
|
print(line.rstrip(), flush=True)
|
|
except Exception as e:
|
|
print(f'[{label} reader err] {e}')
|
|
t = threading.Thread(target=reader, args=(stdout,'out'), daemon=True); t.start()
|
|
err_t = threading.Thread(target=reader, args=(stderr,'err'), daemon=True); err_t.start()
|
|
|
|
t0 = time.time()
|
|
while time.time() - t0 < 7200:
|
|
if stdout.channel.exit_status_ready(): break
|
|
time.sleep(2)
|
|
|
|
t.join(timeout=5)
|
|
err_t.join(timeout=5)
|
|
rc = stdout.channel.recv_exit_status() if stdout.channel.exit_status_ready() else -1
|
|
print(f'\n[exit {rc}]')
|
|
c.close()
|