25 changed files with 24 additions and 2196 deletions
@ -1,390 +0,0 @@ |
|||||
# RADIUS·TACACS·메일·시스템 로그 실습 시연 |
|
||||
|
|
||||
이 문서는 2026-09-09 현재 PC의 기존 AAA 실습 환경과 장비 소스를 확인해서 작성했다. 전체 메뉴 안내는 `DEMO_RUNBOOK_20260909.md`를 참고한다. |
|
||||
|
|
||||
## 1. 지금 준비된 것과 이번에 추가한 것 |
|
||||
|
|
||||
| 항목 | 주소 | 현재 확인 범위 | |
|
||||
|---|---|---| |
|
||||
| 장비 관리 UI | http://192.168.0.223/ | 관리 포트80 기준 | |
|
||||
| 기존 RADIUS 컨테이너 | 192.168.0.100:1645/UDP → 컨테이너1812 | Docker 실행 상태·포트 매핑 확인 | |
|
||||
| 기존 TACACS+ 컨테이너 | 192.168.0.100:49/TCP | Docker 실행 상태·포트 매핑 확인 | |
|
||||
| 새 Mailpit 수신 서버 | PC 1025/TCP, 수신함 http://127.0.0.1:8025/ | 구성 파일 추가, 이번 작업에서 실행·장비 수신 시험은 하지 않음 | |
|
||||
| 새 Syslog 수신 서버 | PC 1514/UDP 또는 TCP → 컨테이너514 | 구성 파일 추가, 이번 작업에서 실행·장비 수신 시험은 하지 않음 | |
|
||||
|
|
||||
RADIUS와 TACACS는 이미 **별도 이미지·별도 컨테이너**다. 두 서버를 바꾸어 켤 필요 없이 함께 실행하고, 장비에서 인증 방식을 선택해 시연하면 된다. 메일·Syslog까지 총 네 컨테이너 구성이다. |
|
||||
|
|
||||
**동시 실행과 동시 인증의 구분** |
|
||||
|
|
||||
- 웹 로그인: `primary_method` 하나를 사용한다. RADIUS → TACACS 순서로 기본 방식을 바꿔서 시험한다. 한 번의 웹 로그인에서 두 서버를 모두 통과하는 구조가 아니다. |
|
||||
- Reverse SSH/Telnet: 포트의 동작 설정별로 RADIUS 또는 TACACS를 지정할 수 있다. 서로 다른 시험 포트/Listen Port로 나누면 나란히 시험할 수 있다. |
|
||||
- 장비 웹의 외부 인증은 Local fallback을 유지한다. 외부 인증 거부 뒤에도 같은 이름·암호의 로컬 계정이 있으면 로그인할 수 있다. 따라서 `labadmin`을 장비 로컬 사용자에 중복 등록하지 않는다. |
|
||||
- 포트 Reverse 외부 인증 경로는 실패 시 로컬 암호로 우회하지 않는다. |
|
||||
- 현재 구현은 인증 결과와 장비에 설정한 기본 역할을 사용한다. RADIUS 속성별 역할 매핑, TACACS 명령별 허가·Accounting, RADIUS Challenge/EAP/MFA 시연으로 확대해 설명하지 않는다. |
|
||||
- 시리얼로 연결한 상대 장비의 자체 로그인은 또 다른 인증이다. 통합콘솔의 Reverse 인증 성공 뒤 상대 장비가 아이디를 요구하는 것은 이중 오류가 아니라 별도 로그인일 수 있다. |
|
||||
|
|
||||
## 2. PC 서버 준비: 복사해서 실행할 명령 |
|
||||
|
|
||||
### 2-1. 주소·컨테이너 확인 |
|
||||
|
|
||||
Docker Desktop을 Linux 컨테이너 모드로 실행하고 PowerShell을 연다. |
|
||||
|
|
||||
```powershell |
|
||||
Set-Location C:\Users\temp\Desktop\tera_Clone_Demo |
|
||||
Get-NetIPAddress -AddressFamily IPv4 |
|
||||
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" |
|
||||
``` |
|
||||
|
|
||||
아래 예시는 PC LAN 주소가 `192.168.0.100`일 때다. 다른 주소면 모든 PC 서버 주소를 그 값으로 바꾼다. **장비의 서버 주소에 localhost, 127.0.0.1, Docker 내부 서비스 이름을 입력하면 안 된다.** |
|
||||
|
|
||||
### 2-2. RADIUS·TACACS |
|
||||
|
|
||||
현재 두 컨테이너가 정상 실행 중이면 시작 명령은 생략한다. 중지됐거나 새로 구성할 때: |
|
||||
|
|
||||
```powershell |
|
||||
.\aaa-lab\start.ps1 -BindAddress 192.168.0.100 |
|
||||
docker compose -f .\aaa-lab\docker-compose.yml ps |
|
||||
``` |
|
||||
|
|
||||
실행 파일은 각각 `aaa-lab/radius/Dockerfile`, `aaa-lab/tacacs/Dockerfile`, 묶음 구성은 `aaa-lab/docker-compose.yml`이다. 서버 계정은 각각 `radius/authorize`, `tacacs/tac_plus.conf`에 있다. 이 구성은 파일을 이미지에 복사하므로 계정을 바꿨다면 다시 빌드해야 반영된다. |
|
||||
|
|
||||
기존 실습 전용 값: |
|
||||
|
|
||||
| 입력 | 값 | |
|
||||
|---|---| |
|
||||
| 사용자 이름 | `labadmin` | |
|
||||
| 암호 | `LabAdminPassword-2026` | |
|
||||
| 서버 공유 Secret | `LabAaaSharedSecret-2026` | |
|
||||
|
|
||||
이는 공개된 폐쇄망 실습 값이다. 실제 운영 자격증명이 아니다. Shared Secret은 사용자 암호와 다른 값이며, 장비와 서버가 동일하게 설정해야 한다. |
|
||||
|
|
||||
### 2-3. 메일·Syslog 수신기 |
|
||||
|
|
||||
다음 명령은 새 이미지 다운로드/빌드와 컨테이너 시작을 수행한다. 인터넷이 없는 시연장에서는 미리 빌드해 둔다. |
|
||||
|
|
||||
```powershell |
|
||||
$env:DEMO_BIND_ADDRESS = '192.168.0.100' |
|
||||
docker compose -f .\demo-services\compose.yaml config --quiet |
|
||||
docker compose -f .\demo-services\compose.yaml up -d --build |
|
||||
docker compose -f .\demo-services\compose.yaml ps |
|
||||
``` |
|
||||
|
|
||||
두 서비스가 실행된 뒤 PC에서 `http://127.0.0.1:8025/`를 열면 시험 메일함이 보인다. Mailpit의 메일함은 이 PC에만 공개했고 SMTP1025는 장비가 접근하는 PC LAN 주소에 공개했다. |
|
||||
|
|
||||
### 2-4. 필요한 방화벽만 확인 |
|
||||
|
|
||||
필요 경로는 **장비192.168.0.223 → PC192.168.0.100**의 UDP1645, TCP49, TCP1025, UDP/TCP1514다. 브라우저용8025는 PC 내부에서만 사용한다. |
|
||||
|
|
||||
차단될 경우 관리자 PowerShell에서 해당 장비만 허용하는 규칙을 추가할 수 있다. 다음은 규칙이 아직 없을 때의 예시다. |
|
||||
|
|
||||
```powershell |
|
||||
New-NetFirewallRule -DisplayName 'TERA Demo TCP' -Direction Inbound -Action Allow -Protocol TCP -LocalAddress 192.168.0.100 -LocalPort 49,1025,1514 -RemoteAddress 192.168.0.223 -Profile Any |
|
||||
New-NetFirewallRule -DisplayName 'TERA Demo UDP' -Direction Inbound -Action Allow -Protocol UDP -LocalAddress 192.168.0.100 -LocalPort 1645,1514 -RemoteAddress 192.168.0.223 -Profile Any |
|
||||
``` |
|
||||
|
|
||||
이 PC에서는 과거 Docker의 명시적 차단 규칙이 허용 규칙보다 우선했던 기록이 있다. 추가 후에도 안 되면 기존 차단 규칙을 확인한다. 방화벽 전체를 끄지 않는다. 기존 `aaa-lab/fix-radius-firewall.ps1`은 과거 특정 규칙을 수정하는 스크립트이므로 현재 규칙·주소를 확인한 뒤 사용한다. `Test-NetConnection -Port`는 TCP 시험이라 RADIUS UDP 정상 여부를 판정할 수 없다. |
|
||||
|
|
||||
## 3. RADIUS 웹 로그인 시연 |
|
||||
|
|
||||
### 화면 준비 |
|
||||
|
|
||||
- 관리자 브라우저: 현재 관리자 로그인 유지. |
|
||||
- 시험 브라우저: 별도 프로필/시크릿 창. 동일 시크릿 세션의 여러 창은 쿠키를 공유할 수 있으므로 각각 독립 세션인지 확인. |
|
||||
- PowerShell: 아래 서버 로그 표시. |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\aaa-lab\docker-compose.yml logs --since 1m --follow radius |
|
||||
``` |
|
||||
|
|
||||
### 장비 입력값 |
|
||||
|
|
||||
관리자 운영 → RADIUS: |
|
||||
|
|
||||
| 항목 | 입력 | |
|
||||
|---|---| |
|
||||
| 사용 | 켬 | |
|
||||
| 서버 | 192.168.0.100 | |
|
||||
| 포트 | **1645** | |
|
||||
| 공유 Secret | LabAaaSharedSecret-2026 | |
|
||||
| 타임아웃 | 5초 | |
|
||||
| 기본 역할 | 포트 운영자 | |
|
||||
|
|
||||
적용 후 재조회한다. 이 실습은 호스트1645를 컨테이너1812로 연결했으므로 장비에1812를 입력하면 다른 경로를 시험하게 된다. |
|
||||
|
|
||||
### 시연 A: 성공 |
|
||||
|
|
||||
1. 장비 로컬 사용자에 `labadmin`이 중복으로 없는지 확인한다. |
|
||||
2. 시험 브라우저에서 `http://192.168.0.223/`에 접속한다. |
|
||||
3. `labadmin` / `LabAdminPassword-2026`과 올바른 CAPTCHA로 로그인한다. |
|
||||
4. 장비 화면의 로그인 성공·포트 운영자 메뉴를 보여 준다. |
|
||||
5. 같은 시각의 RADIUS 로그에서 사용자 요청과 `Access-Accept`를 보여 준다. |
|
||||
|
|
||||
**합격:** 장비 로그인 성공 + 실제 서버의 해당 요청 승인. 화면에 들어갔다는 사실만으로 합격하지 않는다. |
|
||||
|
|
||||
### 시연 B: 잘못된 암호 거부 |
|
||||
|
|
||||
1. 시험 계정 로그아웃 후 새 CAPTCHA를 입력한다. |
|
||||
2. 암호만 `Wrong-Lab-Password`로 바꿔 1회 로그인한다. |
|
||||
3. 장비 로그인 거부와 서버 `Access-Reject`를 확인한다. |
|
||||
4. CAPTCHA 오류로 거부되면 외부 서버 요청 자체가 없을 수 있으므로 암호 거부 시험과 구분한다. |
|
||||
|
|
||||
### 시연 C: 서버 중단과 Local fallback |
|
||||
|
|
||||
다른 사람이 이 시험 서버를 사용하지 않는 시간에만 한다. 기존 세션은 서버를 끈다고 즉시 로그아웃되지 않으므로 **새 로그인**으로 검사한다. |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\aaa-lab\docker-compose.yml stop radius |
|
||||
``` |
|
||||
|
|
||||
- 외부 전용 `labadmin`의 새 로그인은 실패해야 한다. |
|
||||
- 실제 로컬 관리자 계정의 새 로그인은 fallback으로 가능해야 한다. 관리자 이름·암호는 현재 장비 값을 사용한다. |
|
||||
- 기존 관리자 세션은 유지하고, 임계 횟수에 도달할 정도로 외부 계정 실패를 반복하지 않는다. |
|
||||
|
|
||||
끝나면 즉시 복원한다. |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\aaa-lab\docker-compose.yml start radius |
|
||||
``` |
|
||||
|
|
||||
복원 후 새 CAPTCHA로 외부 로그인 성공을 다시 확인한다. 타임아웃과 계정 잠금 상태를 구분한다. |
|
||||
|
|
||||
## 4. TACACS+ 웹 로그인 시연 |
|
||||
|
|
||||
두 서버는 계속 함께 실행한다. 관리자 운영 → TACACS에서 다음을 적용한다. |
|
||||
|
|
||||
| 항목 | 입력 | |
|
||||
|---|---| |
|
||||
| 사용 | 켬 | |
|
||||
| 서버 | 192.168.0.100 | |
|
||||
| 포트 | **49** | |
|
||||
| 공유 Secret | LabAaaSharedSecret-2026 | |
|
||||
| 타임아웃 | 5초 | |
|
||||
| 기본 역할 | 포트 운영자 | |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\aaa-lab\docker-compose.yml logs --since 1m --follow tacacs |
|
||||
``` |
|
||||
|
|
||||
1. 새 시험 세션에서 동일 실습 계정과 올바른 CAPTCHA로 로그인한다. |
|
||||
2. TACACS 서버 로그의 같은 시각 인증 성공 기록과 장비 로그인 성공을 함께 확인한다. 서버 로그 문구는 버전마다 달라 RADIUS의 `Access-Accept`라는 문구를 찾는 것이 아니다. |
|
||||
3. 로그아웃 후 틀린 암호 1회 → 서버의 인증 거부 + 장비 로그인 거부를 확인한다. |
|
||||
4. 필요 시 `docker compose -f .\aaa-lab\docker-compose.yml stop tacacs`로 중단 → 외부 계정 새 로그인 실패 → 로컬 관리자 로그인 가능을 확인한다. |
|
||||
5. `docker compose -f .\aaa-lab\docker-compose.yml start tacacs`로 복원하고 외부 로그인을 재확인한다. |
|
||||
|
|
||||
**주의:** TACACS를 기본으로 적용하면 웹 로그인은 TACACS를 사용한다. RADIUS 화면의 ‘사용’ 표시도 기본 방식 여부를 반영하므로 꺼져 보일 수 있다. 이것만 보고 RADIUS 컨테이너가 종료됐다고 판단하지 않는다. 두 서버 목록을 보존한 채 기본 방식만 바뀌는 흐름이다. |
|
||||
|
|
||||
## 5. 두 인증 방식을 한 화면에서 나란히 시연하기 |
|
||||
|
|
||||
웹 로그인은 위처럼 순서대로 보여 주고, 동시 비교는 Reverse 접속으로 한다. |
|
||||
|
|
||||
1. RADIUS를 먼저 사용·적용하고 TACACS도 사용·적용해 두 서버 정보를 저장한다. 마지막 기본 방식은 TACACS여도 된다. 이후 RADIUS를 명시적으로 사용 해제·저장하지 않는다. |
|
||||
2. 실제 연결된 시험 포트 A의 동작 설정1을 Reverse SSH, 인증 RADIUS로 지정한다. |
|
||||
3. 다른 시험 포트 B의 동작 설정1을 Reverse SSH, 인증 TACACS로 지정한다. |
|
||||
4. 서로 다른 미사용 Listen Port를 선택한다. 예시는 A=22001, B=22002이며 실제 사용 가능 여부를 확인한 후 적용한다. 웹80은 변경하지 않는다. |
|
||||
5. 데이터 포트는 상대와 동일한9600/8/N/1로 맞춘다. |
|
||||
6. PowerShell 창 두 개에서 각각 접속한다. 암호는 프롬프트에서 입력한다. |
|
||||
|
|
||||
```powershell |
|
||||
ssh -p 22001 labadmin@192.168.0.223 |
|
||||
``` |
|
||||
|
|
||||
```powershell |
|
||||
ssh -p 22002 labadmin@192.168.0.223 |
|
||||
``` |
|
||||
|
|
||||
7. 최초 SSH 호스트 키 확인 시 대상 장비의 키인지 확인한다. 장비 변경 뒤 키 불일치가 나면 검증 없이 호스트 키 검사를 끄지 않는다. |
|
||||
8. RADIUS 창 접속 시 radius 로그, TACACS 창 접속 시 tacacs 로그에 인증 기록이 생기는지 보여 준다. |
|
||||
9. 각각 상대 장비의 무해한 조회 명령을 실행하고 실제 응답을 확인한다. |
|
||||
10. 잘못된 암호는 접속을 거부해야 한다. 인증 성공 후 응답이 없는 경우는 시리얼 배선·설정·IRQ 문제로 따로 조사한다. |
|
||||
|
|
||||
**합격:** 각 경로에서 의도한 외부 서버 승인 → Reverse 세션 생성 → 실제 상대 응답. 상대 장비가 한 대뿐이면 순서대로 시험하고 ‘동시 양방향 검증’이라고 설명하지 않는다. |
|
||||
|
|
||||
TACACS 서버 설정에 `priv-lvl=15`가 있어도 현재 장비가 그 값을 사용해 관리 권한을 부여하는 것은 아니다. 장비의 외부 인증 기본 역할을 기준으로 설명한다. 브라우저 포트 디버그만으로 Reverse 인증 방식 검증을 대신하지 않는다. |
|
||||
|
|
||||
## 6. 메일 알림: 실제 수신함까지 보여 주기 |
|
||||
|
|
||||
### 장비 입력값 |
|
||||
|
|
||||
기타 설정 → 알림 → 메일 설정에서 다음을 저장한다. |
|
||||
|
|
||||
| 항목 | 입력 | |
|
||||
|---|---| |
|
||||
| 메일 사용 | 켬 | |
|
||||
| SMTP 서버 | 192.168.0.100 | |
|
||||
| SMTP 포트 | **1025** | |
|
||||
| STARTTLS | 끔 | |
|
||||
| SMTP 인증 | 끔 | |
|
||||
| 발신자 | tera@example.test | |
|
||||
| 수신자 | demo@example.test | |
|
||||
| 참조 | 비워 둠 | |
|
||||
| 제목 | TERA 시연 알림 | |
|
||||
| 본문 | 장비 알림 시험: {{message}} | |
|
||||
|
|
||||
Mailpit은 이 시험망에서 메일을 받아 PC 웹 화면에 보여 주는 수신기다. 주소의 실제 사서함이나 Gmail 계정이 필요하지 않으며 외부 수신함으로 자동 배달하지 않는다. 기본 SMTP는 인증·암호화 없이1025를 사용한다. [Mailpit SMTP 공식 문서](https://mailpit.axllent.org/docs/configuration/smtp/) |
|
||||
|
|
||||
### 시연 A: 테스트 발송 |
|
||||
|
|
||||
1. PC에서 `http://127.0.0.1:8025/`를 연다. |
|
||||
2. 장비에서 설정을 적용하고 **테스트 발송**을 누른다. |
|
||||
3. 새 메일을 열어 수신 시각, 발신자, 수신자, 제목, 본문을 보여 준다. |
|
||||
4. 예전 메일을 성공으로 착각하지 않도록 현재 시각과 비교한다. |
|
||||
|
|
||||
**합격:** 버튼 처리 성공 + Mailpit에 새 메일 도착. 이것은 SMTP 발송 경로 시험이며 자동 이벤트 알림 시험은 다음 단계다. |
|
||||
|
|
||||
### 시연 B: 실제 이벤트에 의한 자동 메일 |
|
||||
|
|
||||
다음 중 한 가지를 먼저 리허설한다. |
|
||||
|
|
||||
**방법 1 — 시리얼 수신 키워드(일반 시연 권장)** |
|
||||
|
|
||||
1. 시험 포트 고급 설정에 `DEMO_ALARM_0909`를 저장한다. |
|
||||
2. 기타 설정 → 알림 → 시리얼 이벤트에서 같은 채널을 선택한다. |
|
||||
3. 수신 키워드 감지와 메일 전달을 켜서 적용한다. |
|
||||
4. 상대 장비에서 `DEMO_ALARM_0909`가 **장비 RX로 들어오도록** 전송한다. 터미널 TX로 보내기만 해서는 수신 조건을 입증하지 못한다. 에코 상대라면 돌아온 RX를 확인한다. |
|
||||
5. 포트 로그의 수신 문자열과 이벤트, Mailpit의 새 메일을 같은 시각으로 대조한다. |
|
||||
|
|
||||
**방법 2 — 로그인 실패 임계값(시스템 이벤트 연계)** |
|
||||
|
|
||||
1. 시스템 이벤트에서 로그인 실패 임계값 알림과 메일 전달을 켠다. |
|
||||
2. 장비의 현재 로그인 보호 횟수·감지 시간을 확인한다. 값을 모르면 임의로 3회라고 가정하지 않는다. |
|
||||
3. 관리자와 다른 전용 실패 시험 이름으로 올바른 CAPTCHA + 틀린 암호를 설정 임계 횟수만큼 시도한다. |
|
||||
4. 임시 잠금 발생 기록과 새 메일을 확인한다. 실제 관리자 계정으로 시험하지 않는다. |
|
||||
5. 해당 시험 계정의 잠금 해제 시간을 기록하고 이후 정상 인증 시연에는 사용하지 않는다. |
|
||||
|
|
||||
정상 로그인이나 설정 변경이 모든 경우 메일로 발송되는 것은 아니다. 알림 규칙에 연결된 이벤트를 만들어야 한다. |
|
||||
|
|
||||
### 실제 회사 메일로 전환할 때 |
|
||||
|
|
||||
회사 SMTP 서버가 제공하는 호스트·포트·인증·발신 허용 조건을 입력하고 외부 사서함에서 도착을 확인한다. 현재 발송 코드는 평문 연결 후 선택적으로 STARTTLS와 사용자/암호 인증을 한다. **465의 암묵적 TLS나 OAuth 전용 서버를 지원한다고 가정하지 않는다.** Mailpit 성공만으로 회사 메일 서버 인증까지 검증된 것은 아니다. |
|
||||
|
|
||||
## 7. 시스템 로그: 사건을 만들고 찾아내는 시연 |
|
||||
|
|
||||
### 7-1. 먼저 기록 설정 |
|
||||
|
|
||||
기타 설정 → 시스템 로그 → 로그 이벤트에서 로컬 시스템·설정 변경·운영 기록을 켜고 적용한다. 시작 시각을 기록한다. 장비와 PC 시각을 맞춰 놓는다. |
|
||||
|
|
||||
### 7-2. 세 가지 사건 생성 |
|
||||
|
|
||||
| 사건 | 실행 | 확인할 내용 | |
|
||||
|---|---|---| |
|
||||
| 인증 성공·실패 | 별도 시험 계정 정상 로그인 후 로그아웃, 틀린 암호로1회 시도 | 사용자, 시각, `auth.login`, succeeded/authentication_failed 등 결과 | |
|
||||
| 설정 변경 | 시험 포트 별칭을 `DEMO_LOG_0909`로 변경·적용 | 설정 변경 분류, 작업자, 포트/대상, 처리 결과 | |
|
||||
| 포트 운영 | 시험 포트 접속·무해한 문자열 송수신·해제 | 포트 운영/포트 로그의 채널·방향·원문 | |
|
||||
|
|
||||
로그 메뉴 → 시스템 로그에서 시작 시각 이후를 검색한다. 종류 필터를 시스템/설정 변경/운영에 맞게 바꾼다. 별칭 문자열이 감사 메시지에 반드시 포함되는 것은 아니므로 안 나오면 사용자·대상 포트·시각으로 찾는다. RX/TX 원문은 포트 로그에서 확인한다. |
|
||||
|
|
||||
### 7-3. 필터·다운로드·권한 |
|
||||
|
|
||||
1. 사용자 또는 시험 시각으로 검색하고 기대한 성공·실패 두 기록을 보여 준다. |
|
||||
2. 페이지 크기와 이전/다음 이동을 보여 준다. |
|
||||
3. 같은 검색 조건으로 CSV/JSON/JSONL을 내려받아 시각·행 내용이 일치하는지 확인한다. 내보내기 상한 때문에 전체 이력과 건수가 다를 수 있다. |
|
||||
4. 포트 운영자 계정으로 시스템 로그 접근이 거부되는지 확인한다. 허용 포트의 포트 로그 접근은 가능해야 한다. |
|
||||
5. 재부팅 후 시험 시각의 기록을 다시 찾아 보관 여부를 확인한다. |
|
||||
|
|
||||
**설명 문장:** “메뉴를 보여 주는 데서 끝내지 않고, 방금 만든 성공·실패·설정 변경이 누가 언제 무엇을 했는지 기록되는지 확인합니다.” |
|
||||
|
|
||||
## 8. 원격 Syslog: 장비 밖에 저장되는 것 확인 |
|
||||
|
|
||||
### 수신 화면 준비 |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\demo-services\compose.yaml exec syslog tail -n 30 -F /var/log/tera/remote.log |
|
||||
``` |
|
||||
|
|
||||
이 창을 시연 화면 옆에 둔다. `docker compose logs syslog`는 데몬 시작 오류 확인용이며 수신 메시지는 위 파일에서 확인한다. |
|
||||
|
|
||||
### 장비 설정 |
|
||||
|
|
||||
기타 설정 → 시스템 로그 → 로그 서버: |
|
||||
|
|
||||
| 항목 | 입력 | |
|
||||
|---|---| |
|
||||
| 원격 사용 | 켬 | |
|
||||
| 로그 서버 | **192.168.0.100:1514** | |
|
||||
| 프로토콜 | 우선 TCP | |
|
||||
| 원격 전송할 로그 | 시스템·설정 변경·운영·상태 중 시험 항목 켬 | |
|
||||
|
|
||||
1. 적용 후 **새로운** 실패 로그인1회 또는 별칭 변경을 수행한다. 과거 이력이 자동 재전송된다고 가정하지 않는다. |
|
||||
2. 장비 로컬 기록과 PC 수신 파일에 같은 시각·사용자·대상 사건이 있는지 비교한다. |
|
||||
3. 장비의 전송 상태 새로고침에서 최근 상태·시도 횟수·오류를 확인한다. |
|
||||
4. UDP도 시연하려면 프로토콜을 UDP로 변경·적용하고 새 사건을 다시 만든다. 동일1514로 받는다. |
|
||||
|
|
||||
**합격:** 실제 수신 파일에 해당 사건이 기록됨. 특히 UDP의 ‘전송 완료’는 수신 증거가 아니다. Docker NAT로 `from=`이 내부 주소로 보일 수 있으므로 메시지 내용·사용자·시각도 대조한다. |
|
||||
|
|
||||
### 선택: 실패·복구 시연 |
|
||||
|
|
||||
TCP 모드에서 수신기를 중단한다. |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\demo-services\compose.yaml stop syslog |
|
||||
``` |
|
||||
|
|
||||
새 시험 사건을 만들고 전송 실패/재시도 상태를 확인한다. 전송 처리 시간에 따라 즉시 바뀌지 않을 수 있다. 다시 시작한다. |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\demo-services\compose.yaml start syslog |
|
||||
``` |
|
||||
|
|
||||
새 사건의 수신을 확인한다. 중단 중 사건까지 모두 복구됐다고 설명하려면 해당 사건도 파일에서 하나씩 확인한다. 무제한 재전송·유실 없음은 보장하지 않는다. |
|
||||
|
|
||||
수신 기록 파일 보관: |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\demo-services\compose.yaml cp syslog:/var/log/tera/remote.log .\demo-services\received-syslog.log |
|
||||
``` |
|
||||
|
|
||||
## 9. 실패했을 때 확인 순서 |
|
||||
|
|
||||
| 증상 | 우선 확인 | |
|
||||
|---|---| |
|
||||
| RADIUS 서버에 요청이 없음 | PC주소, UDP1645 매핑·방화벽, 웹 기본 인증 방식, CAPTCHA에서 먼저 거부됐는지 | |
|
||||
| RADIUS 요청은 있으나 응답 없음 | Shared Secret, 클라이언트 허용, Message-Authenticator 정책. 현재 클라이언트는 PAP이며 Challenge 미지원 | |
|
||||
| Access-Reject | 실제 서버 계정·암호, 로컬 계정 암호와 혼동 여부 | |
|
||||
| 서버가 승인했는데 웹 실패 | 장비 로그·응답 검증·세션 처리 확인. 서버 승인만으로 웹까지 합격하지 않음 | |
|
||||
| TACACS 로그 없음 | TCP49, 기본 방식, 서버 실행, 방화벽 | |
|
||||
| 외부 서버를 꺼도 로그인됨 | 기존 세션인지, 같은 이름의 로컬 계정으로 fallback 됐는지 | |
|
||||
| 포트 인증은 성공하나 RX 없음 | 상대 로그인·9600·배선·실제 응답·현재 IRQ95 상태 | |
|
||||
| 메일 테스트 실패 | SMTP1025와 웹8025 혼동, STARTTLS/인증 체크, 방화벽 | |
|
||||
| 테스트 메일만 오고 자동 메일 안 옴 | 해당 규칙 활성·메일 전달 선택, RX 키워드 또는 실제 임계 조건 충족 | |
|
||||
| Syslog 전송 완료인데 수신 없음 | UDP 결과 의미, 1514포트, 선택 분류, 사건이 설정 이후 생성됐는지 | |
|
||||
| 시스템 로그에서 데이터 원문 안 보임 | 포트 로그를 선택했는지, 해당 포트 RX/TX 기록 옵션 | |
|
||||
|
|
||||
기존 RADIUS 구성은 Docker NAT 실습용 catch-all client와 Message-Authenticator 미요구 설정이다. 현행 장비 클라이언트의 호환 시험용이며 운영망 보안 구성을 충족한다고 소개하지 않는다. 운영 서버가 Message-Authenticator를 필수로 요구하면 클라이언트 개선이 필요할 수 있다. |
|
||||
|
|
||||
## 10. 시연 순서·종료 |
|
||||
|
|
||||
**권장 진행:** 네 서버 준비 → RADIUS 성공/실패 → TACACS 성공/실패 → 두 Reverse 접속 비교 → Mailpit 테스트 수신 → 키워드 자동 메일 → 로그 검색·내보내기 → 원격 Syslog 수신 → 설정 원복. |
|
||||
|
|
||||
- 서버 중단·복구는 기본 시연이 성공한 뒤 선택해서 한다. |
|
||||
- 장비의 외부 기본 인증·기본 역할·포트 인증·메일·로그 목적지·알림 규칙을 시연 전 값으로 복원한다. 무조건 Local로 바꾸는 것이 아니라 기존 상태를 기준으로 한다. |
|
||||
- 수신 증거를 먼저 보관한다. 아래 stop은 컨테이너를 중지하며 볼륨 데이터를 삭제하지 않는다. |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\demo-services\compose.yaml stop |
|
||||
``` |
|
||||
|
|
||||
AAA도 종료하려면 다른 사용자가 없는지 확인한 뒤: |
|
||||
|
|
||||
```powershell |
|
||||
docker compose -f .\aaa-lab\docker-compose.yml stop |
|
||||
``` |
|
||||
|
|
||||
이번 시연용으로 만든 방화벽 규칙만 제거하려면 관리자 PowerShell에서: |
|
||||
|
|
||||
```powershell |
|
||||
Get-NetFirewallRule -DisplayName 'TERA Demo TCP','TERA Demo UDP' | Remove-NetFirewallRule |
|
||||
``` |
|
||||
|
|
||||
### 이전 문서·스크립트와의 차이 |
|
||||
|
|
||||
`aaa-lab/README.md`와 예전 원격 시험 스크립트에는 `:8083` 주소와 CAPTCHA 없이 로그인하는 흐름이 남아 있다. 이번 시연은 **HTTP80 + 실제 CAPTCHA 수동 로그인** 기준이며, 그 스크립트를 그대로 실행하지 않는다. 이 PC에 실행 중인 인증 컨테이너가 있다는 사실은 현재 교체 장비의 인증 성공을 이번에 재검증했다는 뜻이 아니다. |
|
||||
|
|
||||
### 근거 |
|
||||
|
|
||||
- 로컬: `aaa-lab/docker-compose.yml`, `radius/authorize`, `radius/clients.conf`, `tacacs/tac_plus.conf` 및 실제 Docker 포트 조회. |
|
||||
- 구현: `backend/internal/externalauth/client.go`, `backend/internal/adapters/mock/store.go`, `backend/internal/alarmdelivery/transport.go`, `frontend/web/static/js/integration.js`. |
|
||||
- [FreeRADIUS 구성 파일](https://wiki.freeradius.org/config/Configuration-files) |
|
||||
- [현재 실습 TACACS 구현의 설정 문법](https://github.com/facebook/tac_plus/blob/main/tacacs-F4.0.4.28/tac_plus.conf.5.in) |
|
||||
- [Mailpit Docker 구성](https://mailpit.axllent.org/docs/install/docker/) |
|
||||
- [Mailpit SMTP 기본 동작](https://mailpit.axllent.org/docs/configuration/smtp/) |
|
||||
- [rsyslog UDP 입력](https://docs.rsyslog.com/doc/configuration/modules/imudp.html), [TCP 입력](https://docs.rsyslog.com/doc/configuration/modules/imtcp.html) |
|
||||
@ -1,290 +0,0 @@ |
|||||
# TERA 장비 전체 기능 시연·검증 가이드 |
|
||||
|
|
||||
작성 기준: 2026-09-09, 현재 저장소의 화면·처리 코드 및 기존 장비 확인 기록. |
|
||||
|
|
||||
RADIUS·TACACS 서버 실행, 장비 입력값, 성공·실패 시험, 메일 수신함, 로컬/원격 시스템 로그의 구체적인 실습은 [인증·메일·로그 상세 시연](DEMO_AUTH_MAIL_LOG_20260909.md)을 참고한다. |
|
||||
|
|
||||
> 이 문서는 시연 절차다. 모든 기능이 현재 교체 장비에서 실측 검증됐다는 뜻은 아니다. 특히 외부 인증·메일·SNMP·DDNS, 전체 48포트 통신, 펌웨어 갱신·공장 초기화는 별도 환경과 실장비 검증이 필요하다. |
|
||||
|
|
||||
## 1. 시연 구성과 합격 기준 |
|
||||
|
|
||||
권장 구성은 **핵심 시연 약 40분 + 외부 연동·전체 포트 검수 별도**다. 시간은 진행 계획이며 부팅·응답 시간 보장이 아니다. |
|
||||
|
|
||||
| 순서 | 내용 | 계획 시간 | 핵심 증거 | |
|
||||
|---|---|---:|---| |
|
||||
| 1 | 장비 구성 소개, 부팅·시간·로그인 | 5분 | 준비 완료, CAPTCHA, 실제 시각 | |
|
||||
| 2 | 대시보드·포트 상태 | 3분 | 연결한 채널과 표시 일치 | |
|
||||
| 3 | 9600 설정·CR 감지·실제 양방향 통신 | 10분 | 상대 장비 수신 + 장비 RX/TX | |
|
||||
| 4 | 포트 설정·별칭·Reverse 접속 | 5분 | 저장 후 재조회, 실제 접속 | |
|
||||
| 5 | 사용자·권한·로그 | 7분 | 제한 계정 거부, 로그 검색·내보내기 | |
|
||||
| 6 | 준비한 외부 연동 1~2개 | 5분 | 수신 서버의 실제 결과 | |
|
||||
| 7 | 재부팅·설정 유지·초기 응답 재확인 | 5분 이상 | 유지된 설정, 재접속·재통신 | |
|
||||
|
|
||||
**판정 원칙** |
|
||||
|
|
||||
- 설정 저장 성공: 다시 조회했을 때 값이 유지된다. |
|
||||
- 기능 성공: 실제 상대 장비·외부 서버에서 결과를 확인한다. |
|
||||
- 영구 저장 성공: 재부팅 후에도 값과 동작이 유지된다. |
|
||||
- 준비 완료: 백엔드의 준비 조건을 충족한 상태다. 모든 케이블·상대 장비·외부 서비스의 정상 동작까지 보증하는 표시로 설명하지 않는다. |
|
||||
- 미연결 포트가 DOWN/미감지인 것은 정상일 수 있다. 48포트 모두 UP일 필요는 없다. |
|
||||
- 환경이 없거나 실제 확인하지 않은 기능은 **미검증**으로 기록한다. 성공 표시만 보고 합격 처리하지 않는다. |
|
||||
|
|
||||
## 2. 시연 전 준비 |
|
||||
|
|
||||
### 기본값과 준비물 |
|
||||
|
|
||||
| 항목 | 준비·확인 | |
|
||||
|---|---| |
|
||||
| 관리 접속 | `http://192.168.0.223/`, HTTP 기본 포트 **80** 유지 | |
|
||||
| PC 네트워크 | 장비에 도달 가능한 주소·케이블 확인. 이전 PC 주소는 192.168.0.100이었으나 현재 값은 다시 확인 | |
|
||||
| 복구 콘솔 | 현재 구성 COM4 / 115200. USB 연결 변경 시 COM 번호 재확인 | |
|
||||
| 데이터 포트 | 시연 대상 **9600 / 8 / None / 1 / 흐름 제어 None**, 상대 장비와 동일하게 설정 | |
|
||||
| 시리얼 상대 | 실제 명령에 응답하는 시험 장비 또는 검증된 에코 시험기, 맞는 RS232 배선 | |
|
||||
| 계정 | 관리자 계정, 임시 port_operator 계정, 서로 다른 브라우저 프로필 | |
|
||||
| 자료 | 기존 설정 백업, 시연 대상 채널·배선표, 결과 기록표 | |
|
||||
| 선택 연동 | SMTP 수신함, SNMP 매니저·Trap 수신기, Syslog 서버, RADIUS/TACACS 서버, DDNS 계정 | |
|
||||
|
|
||||
**관리 콘솔 115200과 데이터 포트 9600은 서로 다른 설정이다.** CR 감지 때문에 COM4 콘솔 속도까지 9600으로 바꾸지 않는다. |
|
||||
|
|
||||
### 시작 전 반드시 한 번 리허설 |
|
||||
|
|
||||
1. 아래 표에 시험 채널을 확정한다. 이전에 동작한 채널 번호만 믿지 말고 현재 케이블을 확인한다. |
|
||||
2. 가능하면 CH01~CH24 중 한 포트, CH25~CH48 중 한 포트에서 각각 실제 통신한다. |
|
||||
3. 일반 시연은 확인된 포트를 사용한다. 전체 포트 검수는 48개 모두 별도 기록한다. |
|
||||
4. 포트 설정의 **적용 대상 추가/선택 목록**을 확인한다. 선택이 브라우저에 유지될 수 있으므로 실수로 여러 포트에 적용하지 않는다. |
|
||||
5. 설정 백업 파일과 복원 암호를 준비하고 현재 관리 IP·계정·배선 정보를 기록한다. |
|
||||
6. 외부 서버를 사용하는 기능은 시연 전에 해당 서버의 수신 화면까지 열어 둔다. |
|
||||
|
|
||||
| 용도 | 실제 채널 | 상대 장비 | 연결 방식·응답 명령 | |
|
||||
|---|---|---|---| |
|
||||
| 주 시연 | CH____ | ____ | ____ | |
|
||||
| 반대 그룹 확인 | CH____ | ____ | ____ | |
|
||||
| 권한 거부 시험 | CH____ | ____ | 접근 금지 대상으로 지정 | |
|
||||
|
|
||||
## 3. 부팅 → 시간 → 로그인 |
|
||||
|
|
||||
**설명 문장:** “장비가 설정을 적용하는 동안 진행 상태를 보여 주고, 준비가 완료되면 로그인할 수 있습니다.” |
|
||||
|
|
||||
1. 시연 장비를 재부팅하고 시간을 잰다. PC 브라우저에서 관리 주소를 연다. |
|
||||
2. 설정 중 표시·경과 시간·진행률과 로그인 버튼 상태를 확인한다. |
|
||||
3. 준비 완료 시점을 기록한다. 매번 같은 초에 끝나야 한다고 설명하지 않는다. |
|
||||
4. CAPTCHA가 표시되는지 확인한다. 갱신 조작이 제공되면 새 이미지로 바뀌는지도 확인한다. |
|
||||
5. 임시 시험 계정으로 CAPTCHA 오입력을 한 번 시험하고 로그인이 거부되는지 확인한다. 무작정 반복해 잠금·알림을 유발하지 않는다. |
|
||||
6. 올바른 계정·암호·CAPTCHA로 로그인한다. 실패 후에는 새 CAPTCHA가 필요할 수 있다. |
|
||||
7. 날짜와 시간 메뉴에서 PC와 장비의 실제 시각을 비교한다. |
|
||||
|
|
||||
**시간 확인:** 현재 설계에는 장비 시각이 초기화되지 않았을 때 로그인 전 PC 시각을 전달하는 흐름이 있다. 브라우저를 열지 않은 상태에서도 항상 PC 시간으로 맞춰진다고 설명하면 안 된다. NTP 사용 체크와 서버 주소 저장도 실제 동기화 성공과 다르다. 재부팅 후 실제 시각을 확인한다. |
|
||||
|
|
||||
**합격:** 준비 전 로그인 제한, 완료 후 정상 로그인, CAPTCHA 확인, 실제 시각 정상. 이미지가 없거나 준비가 계속 멈추면 시연을 진행하기 전에 원인을 확인한다. |
|
||||
|
|
||||
## 4. 대시보드·공통 UI·포트 상태 |
|
||||
|
|
||||
1. 대시보드에서 CPU·메모리 등 표시되는 자원 정보와 포트 상태를 확인한다. |
|
||||
2. 케이블 연결표와 포트 상태를 대조한다. CR 응답에 따른 상태는 단순 케이블 삽입만으로 즉시 UP이 되지 않을 수 있다. |
|
||||
3. 대시보드 → 포트 설정 → 로그 → 포트 상태를 이동한다. 메뉴 내용과 마지막 선택 메뉴가 일치하는지 확인한다. |
|
||||
4. 작업 탭 생성·전환·닫기, 사이드바 접기, 한국어/English 변경을 확인하고 한국어로 복원한다. |
|
||||
5. 화면에 표시된 초기값이 실제 설정 조회 후 다른 값으로 바뀌는 동안 성급하게 적용하지 않는다. |
|
||||
|
|
||||
**성능 기록:** 부팅 직후, 로그인 직후, 로그인 1분 후 같은 메뉴를 이동해 클릭부터 실제 데이터 표시까지 각각 기록한다. 예를 들어 ‘3초 이내’ 같은 기준을 쓸 경우 시연 전에 합의한 목표로 명시한다. 현재 제품의 보장 수치로 소개하지 않는다. |
|
||||
|
|
||||
## 5. 포트 매개변수·CR 감지·실제 통신 |
|
||||
|
|
||||
**설명 문장:** “통신 속도를 상대 장비와 맞추고, 상태 표시뿐 아니라 실제 데이터를 주고받는 것까지 확인하겠습니다.” |
|
||||
|
|
||||
1. 포트 설정에서 시험 채널을 **개별 보기**로 선택한다. |
|
||||
2. 적용 대상이 시험 채널만인지 확인하고 `9600 / 8 / None / 1 / None`으로 적용한다. |
|
||||
3. 다른 메뉴에 갔다 돌아와 같은 값을 다시 확인한다. 일반 설정 폼은 적용·영구 저장 흐름을 사용하지만 오류 메시지가 나오면 저장 완료로 판단하지 않는다. |
|
||||
4. CR에 응답하는 상대 장비를 연결하고 포트 상태 갱신을 기다린다. 감지까지 걸린 시간을 기록한다. |
|
||||
5. **포트 디버그 → 채널 선택 → 접속**을 누른다. |
|
||||
6. 상대 장비에 맞는 무해한 조회 명령을 입력하고 Enter로 CR을 전송한다. 명령을 모르면 임의 명령 대신 시험용 에코 장치를 사용한다. |
|
||||
7. 상대 장비가 실제로 명령을 수신했는지, 응답이 터미널에 돌아오는지 확인한다. |
|
||||
8. 고급 기능에서 ASCII/HEX·끝 문자 없음/CR/LF/CRLF를 시험한다. 시험 문자열 예시는 `DEMO_20260909_A`, HEX 대응 예시는 `44 45 4D 4F`다. 실제 상대가 허용하는 입력만 보낸다. |
|
||||
9. 터미널의 로그 저장으로 파일을 내려받아 수신 내용이 있는지 확인한다. |
|
||||
10. 접속을 해제하고 다시 접속해 동일하게 통신한다. 두 번째 포트 그룹도 같은 절차로 확인한다. |
|
||||
|
|
||||
**합격:** 상대 수신 + 응답 수신 + 해당 채널 로그의 방향·내용 일치. 터미널에 입력한 문자가 보이는 것만으로 RX 성공으로 판정하지 않는다. |
|
||||
|
|
||||
**추가 확인:** ‘화면 지우기’와 터미널 고급 기능의 ‘로그 지우기’는 각각 화면/터미널 수집 내용의 정리 기능으로 확인한다. 중앙 로그 메뉴의 보관 데이터 삭제와 동일하다고 설명하지 않는다. Ctrl+C·Break 등은 상대 장비 작업을 중단시킬 수 있으므로 일반 시연에서는 사용하지 않는다. |
|
||||
|
|
||||
## 6. 나머지 포트 기능 |
|
||||
|
|
||||
| 기능 | 시연 방법 | 합격 증거 | |
|
||||
|---|---|---| |
|
||||
| 별칭 | 진단 도구의 포트 별칭 또는 포트 매개변수에서 시험 별칭 저장 | 재조회한 별칭 일치 | |
|
||||
| 다중 적용 | 비어 있는 시험 포트 2개만 선택해 동일 설정 적용 | 두 포트 재조회, 비선택 포트 유지 | |
|
||||
| 매개변수 | 별도 상대에서 속도·패리티·흐름 제어 등을 쌍으로 변경 | 상대 설정과 일치할 때 송수신, 종료 후 9600 기준 복원 | |
|
||||
| 동작 설정 1·2 | 각각 화면에 표시된 Listen Port와 Reverse SSH/Telnet 모드 확인·설정 | PC 클라이언트에서 해당 주소·포트 접속 및 실제 시리얼 응답 | |
|
||||
| 모드 비활성화 | 시험 접속 모드를 disabled로 적용 | 해당 서비스 접속 차단, 원복 후 재접속 | |
|
||||
| 인증 방식 | 우선 Local로 시험, 외부 인증은 준비된 서버로 별도 시험 | 정상 계정 성공·잘못된 계정 거부 | |
|
||||
| 최대 클라이언트 | 시험 포트의 제한을 정하고 여러 독립 세션 접속 | 설정된 한도와 실제 접속 결과 일치 | |
|
||||
| 데이터 로그 | 읽기/송신 기록을 켜고 고유 문자열 송수신 | RX/TX 기록, 시간·방향·표시 옵션 확인 | |
|
||||
| 고급 키워드 | `DEMO_ALARM` 등록, 시리얼 이벤트의 키워드 알림도 활성화 | 실제 수신 데이터와 일치하는 이벤트 발생 | |
|
||||
|
|
||||
Reverse 접속용 포트는 화면에 저장된 실제 값을 사용한다. 관리 웹 포트 80과 혼동하지 않는다. SSH/Telnet 접속 성공만으로 실제 시리얼 응답 성공을 대신하지 않는다. 키워드는 대소문자를 무시하는 문자열 포함 방식이며 정규식으로 소개하지 않는다. |
|
||||
|
|
||||
## 7. 사용자·권한·비밀번호 |
|
||||
|
|
||||
1. 관리자 운영 → 사용자에서 임시 `port_operator` 계정을 만든다. 실제 비밀번호는 시연 자료에 적지 않는다. |
|
||||
2. 시험 포트 A만 허용하고 포트 B는 허용하지 않는다. 그룹 권한과 개별 권한을 함께 확인한다. |
|
||||
3. 별도 브라우저 프로필에서 임시 계정으로 로그인한다. |
|
||||
4. 허용 포트의 조회·디버그·로그 접근을 확인한다. |
|
||||
5. 비허용 포트 접근은 거부되는지 확인한다. 메뉴가 안 보이는 것만 확인하지 말고 해당 화면 직접 접근도 시험한다. |
|
||||
6. 관리자 운영, 시스템 로그 등 관리자 전용 기능에 접근할 수 없는지 확인한다. |
|
||||
7. 임시 계정의 비밀번호를 변경하고 로그아웃 후 새 비밀번호로 로그인한다. 이전 비밀번호는 거부되어야 한다. |
|
||||
8. 관리자에서 임시 계정을 비활성화하고 **새 로그인**이 거부되는지 확인한다. 기존 세션 종료 정책은 별도로 관찰해 기록한다. |
|
||||
9. 시연 종료 후 임시 계정·그룹을 정리한다. |
|
||||
|
|
||||
현재 UI의 역할은 administrator와 port_operator를 기준으로 설명한다. 별도 Viewer 역할이 있다고 소개하지 않는다. |
|
||||
|
|
||||
## 8. 로그 전체 확인 |
|
||||
|
|
||||
1. 시스템 로그, 포트 로그, 포트 상태 이벤트 화면을 각각 연다. |
|
||||
2. 앞서 생성한 로그인·설정 변경·송수신·연결 상태 변화 중 실제 제공되는 기록을 찾는다. |
|
||||
3. 시간·포트·사용자·방향·종류·키워드 필터를 각각 적용한다. `DEMO_20260909_A`로 송수신 기록을 식별한다. |
|
||||
4. 페이지 크기와 이전/다음 이동을 확인한다. |
|
||||
5. 동일 조건으로 CSV·JSON·JSONL을 내려받아 시험 문자열·시각·채널·방향을 확인한다. 내보내기 건수 제한 안에서 화면 필터와 일치하는지 비교한다. |
|
||||
6. 운영자 계정에는 허용 포트 로그만 보이는지 확인한다. |
|
||||
7. 보존 설정과 저장소 사용량을 확인한다. 보존 기간은 설정값이며 실제 그 기간의 기록이 모두 존재한다는 증거는 아니다. |
|
||||
8. 삭제는 필요한 경우 마지막에 시험 포트의 폐기 가능한 로그로만 한다. 보호된 감사 로그는 일반 삭제 대상으로 소개하지 않는다. |
|
||||
|
|
||||
보존 기간을 줄이면 오래된 기록이 지워질 수 있으므로 일반 시연에서는 현재 값을 유지한다. 저장소 용량·발생량에 따른 실제 보존 가능 기간도 설정 기간과 구분한다. |
|
||||
|
|
||||
## 9. 기타 설정·외부 알림 |
|
||||
|
|
||||
| 메뉴 | 준비·실행 | 성공 판정 | |
|
||||
|---|---|---| |
|
||||
| 날짜와 시간 | Asia/Seoul, NTP 설정 확인. 필요 시 ‘현재 PC 시간으로 동기화’ | 실제 시각 일치 및 재부팅 후 재확인 | |
|
||||
| NTP | 도달 가능한 외부 또는 내부 서버 사용 | 설정 저장 외에 실제 동기화 상태·시각 확인 | |
|
||||
| 메일 | SMTP 주소·포트·TLS·인증·수신자 설정 후 테스트 발송 | 실제 수신함 도착·제목·내용 일치 | |
|
||||
| SNMP Trap | 수신기 주소·버전·인증 설정 후 테스트 전송 | 수신기의 Trap 확인 | |
|
||||
| 원격 Syslog | 서버·UDP/TCP·대상 이벤트 설정 후 시험 이벤트 생성 | 원격 서버의 해당 이벤트 확인 | |
|
||||
| 시스템 이벤트 | 준비된 이벤트 하나를 안전하게 발생 | 로컬 기록과 선택한 알림 채널의 실제 수신 | |
|
||||
| 시리얼 키워드 이벤트 | 시험 포트 키워드·전달 채널 설정 후 상대에서 문자열 전송 | 해당 포트 키워드 이벤트 및 실제 수신 | |
|
||||
| DCD/DSR 이벤트 | 지원·배선이 확인된 시험 장비로 신호 변경 | 신호 변화와 이벤트 일치 | |
|
||||
|
|
||||
UDP 전송 성공 표시는 수신 서버 도착을 보장하지 않는다. 로컬/원격 기록 대상 선택, 메일/Trap/Syslog 전달 선택을 모두 확인한다. DCD/DSR은 현재 배선·보드 지원 확인 없이는 성공을 약속하지 않는다. |
|
||||
|
|
||||
## 10. 관리자 운영의 외부 연동·인증서 |
|
||||
|
|
||||
| 기능 | 실행 순서 | 완료 증거·주의점 | |
|
||||
|---|---|---| |
|
||||
| RADIUS | 준비된 서버 주소·포트·공유 비밀·역할·Local fallback 저장 → 시험 계정 인증 | 실제 서버 인증 기록 + 정상/오류 계정 결과. 로컬 관리자 접근 경로 유지 | |
|
||||
| TACACS | 서버 설정 → 실제 서버 계정 인증 | 서버 기록 + 허용 역할·접속 범위 일치 | |
|
||||
| 주소 필터 | 현재 관리자 PC를 고려해 시험 규칙 구성 → 두 번째 PC로 차단 검증 | 허용 PC 접근 가능, 차단 PC 접근 거부. 원복·콘솔 경로 확보 | |
|
||||
| SNMP Agent | v1/v2c/v3 중 준비한 방식 설정 → 매니저에서 조회 | 장비 식별/가동 시간 등 실제 응답. Trap 수신과 별도 시험 | |
|
||||
| SSL 자체 서명 | 장비 시각 확인 → 시험 인증서 생성 → 목록·활성 상태 확인 | HTTPS에서 실제 제공되는 인증서 확인. HTTP80 화면만으로 TLS 검증 불가 | |
|
||||
| SSL 가져오기 | 일치하는 PEM 인증서·개인키 가져오기 | 정상 등록·활성화·HTTPS 인증서 일치 | |
|
||||
| CSR | 입력값으로 CSR 생성·다운로드 | 파일 존재·입력 정보 확인. 발급 완료 인증서와 구분 | |
|
||||
|
|
||||
주 시연은 HTTP80을 유지한다. HTTPS 시험은 활성화된 별도 접속 주소와 인증서 신뢰 조건을 먼저 확인한 뒤 수행한다. 자체 서명 인증서는 브라우저 신뢰 경고가 발생할 수 있다. 외부 인증 서버 장애 시 fallback 동작은 실제 설정·정책대로 시험하며 무조건 로그인 가능하다고 설명하지 않는다. |
|
||||
|
|
||||
## 11. 네트워크·진단 도구 |
|
||||
|
|
||||
일반 시연에서는 **현재 관리 주소와 포트 80을 유지하고 조회·설명 위주**로 진행한다. 실제 네트워크 전환은 별도 검수 시간에 한다. |
|
||||
|
|
||||
| 메뉴 | 확인 방법 | 실제 변경 시 판정 | |
|
||||
|---|---|---| |
|
||||
| 인터페이스 1·2 | DHCP/정적 IP·마스크·게이트웨이·DNS 확인 | 적용 후 의도한 주소로 재접속, 재부팅 후 유지 | |
|
||||
| IPv6 | Auto/Disable/Static과 주소·prefix 확인 | IPv6 시험 PC에서 실제 도달 확인 | |
|
||||
| 호스트 | 시험 호스트명 적용·재조회 | 저장된 이름 확인, 사용하는 이름 해석 경로가 있으면 별도 확인 | |
|
||||
| Single/OOB | 주·대체 인터페이스, 감시 대상·주기·실패 횟수 확인 | 별도 회선에서 주 경로 단절 → 대체 경로 → 복구 실측 | |
|
||||
| Bonding | 모드·링크 감지 설정, 대응 스위치·배선 준비 | 링크 장애 시 설정 모드에 맞는 통신 유지·복구 | |
|
||||
| Bridge | 연결 토폴로지·STP 설정 검토 | 시험망에서 실제 브리지 전달·복구 확인 | |
|
||||
| DDNS | 제공자 계정·도메인·IPv4/6 조건 설정 | 실제 DNS 조회 결과와 업데이트 대상 주소 일치 | |
|
||||
| Ping | 응답 가능한 PC/시험 서버 지정, 횟수 설정 | 실제 응답·손실률 확인. 대상 방화벽의 ICMP 허용 확인 | |
|
||||
| 포트 별칭 | 시험 채널 이름 수정 | 해당 포트 재조회 시 같은 이름 | |
|
||||
|
|
||||
DHCP·IP·OOB·Bonding·Bridge·주소 필터는 현재 세션을 끊을 수 있다. 변경 전 새 접속 주소와 복구 콘솔을 준비한다. 단순 저장 성공은 회선 전환·DNS 갱신 성공의 증거가 아니다. |
|
||||
|
|
||||
## 12. 백업·복원·재부팅 |
|
||||
|
|
||||
### 일반 시연에 적합한 백업·복원 |
|
||||
|
|
||||
1. 기준 상태의 시험 포트 별칭을 기록한다. |
|
||||
2. 백업 내보내기에서 암호를 지정해 설정 파일을 내려받는다. 암호는 별도 보관한다. |
|
||||
3. 시험 별칭을 다른 이름으로 변경한다. |
|
||||
4. 백업 가져오기/복원에서 파일과 암호를 지정한다. 일반 시연은 **네트워크 포함을 끈 상태**로 진행한다. |
|
||||
5. 복원·적용 후 기준 별칭이 돌아왔는지 확인한다. 계정 등 다른 설정도 복원 범위에 영향을 받을 수 있으므로 시험용 백업을 사용한다. |
|
||||
|
|
||||
설정 백업은 펌웨어 전체 이미지나 모든 로그의 백업과 동일하지 않다. |
|
||||
|
|
||||
### 저장 후 재시작 및 영구 저장 확인 |
|
||||
|
|
||||
1. 시험 별칭·9600·사용자 권한·로그 옵션 등 확인할 값을 기록한다. |
|
||||
2. 관리자 상단의 저장 후 재시작을 실행한다. |
|
||||
3. 같은 HTTP80 주소에 다시 접속하고 준비 상태·경과 시간을 확인한다. |
|
||||
4. CAPTCHA로 로그인하고 기록한 값을 다시 조회한다. |
|
||||
5. 실제 송수신을 다시 수행한다. 메뉴 응답 시간·CPU도 같은 조건에서 기록한다. |
|
||||
6. 로그아웃 후 보호된 화면 재접근 시 로그인 요구를 확인한다. |
|
||||
|
|
||||
## 13. 펌웨어·공장 초기화는 별도 마지막 시험 |
|
||||
|
|
||||
### 펌웨어 |
|
||||
|
|
||||
- 현재 장비와 호환되는 **서명된 `.pkg`**가 있을 때만 시험한다. |
|
||||
- `build/linux-arm64`의 실행 파일은 웹 펌웨어 업로드용 패키지와 같지 않다. |
|
||||
- 현재 버전·복구 방법·백업 확보 → 패키지 업로드 → 검증/적용 완료 확인 → 재접속 → 실제 버전 및 핵심 통신 재확인 순서다. |
|
||||
- 업로드 100%는 업데이트 완료가 아니다. 교체 장비의 업데이트 경로는 이번 문서 작성으로 검증된 것이 아니다. |
|
||||
|
|
||||
### 공장 초기화 |
|
||||
|
|
||||
- 사용자·설정·로그·인증서 등을 지우고 재부팅하는 시험이므로 주 시연 종료 후 별도 장비에서 수행한다. |
|
||||
- 현재 구현 계약상 확인 문구는 `공장 초기화`다. 실행 전 백업, 초기 계정 정책, 초기 네트워크 주소, 콘솔 복구를 확인한다. |
|
||||
- 초기화 후 `192.168.0.223`에 그대로 접속된다고 가정하지 않는다. |
|
||||
- 초기 설정·로그인·사용자 데이터 제거·핵심 통신을 확인한다. 현재 교체 장비의 전체 초기화 연동은 별도 검증 대상으로 둔다. |
|
||||
|
|
||||
## 14. 현재 성능·커널 이슈를 설명하는 방법 |
|
||||
|
|
||||
“웹 로그인 진행 표시의 불필요한 애니메이션 부하는 개선했습니다. 기존 기록에서 CPU가 약 41~44%였던 조건이 수정 후 짧은 측정에서는 약 11~14%였습니다. 다만 재부팅 중 RCU stall과 시리얼 IRQ95 차단 기록은 별도 커널 문제로 남아 있어, 실제 포트 통신과 부팅 직후 응답은 계속 확인해야 합니다.” |
|
||||
|
|
||||
- 위 수치는 과거 특정 조건의 측정이며 현재 모든 상황의 보장값이 아니다. |
|
||||
- IRQ95 차단이 발생한 상태에서 통신이 실패하면 웹 설정 완료 표시만 보고 정상이라고 설명하지 않는다. |
|
||||
- 문제 발생 시 시각·채널·요청·증상과 전체 콘솔 로그를 함께 기록한다. 웹 변경만으로 커널 문제가 해결됐다고 단정하지 않는다. |
|
||||
- 정상 시연 전 재부팅 후 실제 송수신 리허설을 통과해야 한다. |
|
||||
|
|
||||
## 15. 결과 기록표 |
|
||||
|
|
||||
결과는 `합격 / 실패 / 미검증 / 해당 없음` 중 하나로 작성한다. |
|
||||
|
|
||||
| 시험 | 결과 | 증거·측정값 | |
|
||||
|---|---|---| |
|
||||
| 부팅 준비·시간·CAPTCHA·로그인 | | 준비 __초, 시각 차이 __초 | |
|
||||
| 부팅 직후/1분 후 UI 응답 | | 각각 __초 / __초, CPU __% | |
|
||||
| 대시보드·상태와 배선 일치 | | 채널 __ | |
|
||||
| 9600 적용·재조회 | | 채널 __ | |
|
||||
| CH01~24 그룹 실제 RX/TX | | 채널 __, 로그 파일 __ | |
|
||||
| CH25~48 그룹 실제 RX/TX | | 채널 __, 로그 파일 __ | |
|
||||
| ASCII/HEX·CR/CRLF | | 상대 수신 __ | |
|
||||
| Reverse SSH/Telnet·접속 한도 | | 모드 __, Listen Port __ | |
|
||||
| 별칭·다중 적용 | | 대상 __ | |
|
||||
| 계정·권한·비밀번호·로그아웃 | | 허용 __, 거부 __ | |
|
||||
| 로그 필터·내보내기·권한 | | 파일 __ | |
|
||||
| 시간 동기화 | | 서버/PC __, 실제 시각 __ | |
|
||||
| SMTP·Trap·Syslog | | 실제 수신 기록 __ | |
|
||||
| 키워드·시스템·신호 이벤트 | | 이벤트 __ | |
|
||||
| RADIUS/TACACS | | 서버 기록 __ | |
|
||||
| SNMP 조회·SSL/CSR | | 응답/인증서 __ | |
|
||||
| 네트워크·DDNS·Ping | | 실제 도달/조회 __ | |
|
||||
| 백업·복원 | | 복원 전후 값 __ | |
|
||||
| 재부팅 후 유지·실제 재통신 | | 결과 __ | |
|
||||
| 펌웨어·공장 초기화 | | 별도 시험 여부 __ | |
|
||||
|
|
||||
전체 48포트 검수는 아래 한 줄을 채널마다 복제해 작성한다. 두 그룹에서 한 개씩 성공한 것만으로 전체 48개 합격으로 처리하지 않는다. |
|
||||
|
|
||||
`CH__ | 상대 장비 __ | 9600/8/N/1 __ | CR 응답 __ | TX __ | RX __ | Reverse __ | 로그 __ | 재부팅 후 __ | 판정 __` |
|
||||
|
|
||||
## 16. 종료 전 원복 |
|
||||
|
|
||||
- 시험 포트 설정을 합의한 기본값으로 복원하고 다중 적용 선택을 해제한다. |
|
||||
- 임시 계정·알림 수신처·키워드·접속 세션을 정리한다. |
|
||||
- 관리 주소·HTTP80, 기준 데이터 속도9600을 다시 확인한다. |
|
||||
- 결과표와 로그·내보내기 파일을 보관한다. 비밀번호·공유 비밀·개인키는 시연 자료에서 제외한다. |
|
||||
- 미검증 항목은 그대로 남겨 후속 검수 대상으로 전달한다. |
|
||||
|
|
||||
### 근거 문서 |
|
||||
|
|
||||
- `backend/BROWSER_ANIMATION_DIAGNOSIS_20260909.md`: 브라우저 부하 비교·수정 기록. |
|
||||
- `backend/REPLACEMENT_UI_PERFORMANCE_20260909.md`: 교체 장비 성능·배포 조사 기록. |
|
||||
- `WEB_LOG_FEATURES.md`: 로그 기능·권한·검증 범위. |
|
||||
- `FACTORY_RESET.md`: 초기화 동작 계약·관련 범위. |
|
||||
- `frontend/internal/mock/mock_data.go`, `frontend/web/static/js/integration.js`, `frontend/web/templates/partials/block_serial-terminal.html`: 메뉴·설정 처리·터미널 조작의 소스 기준. 화면 기본 예제 값은 실장비 값과 구분했다. |
|
||||
@ -1,49 +0,0 @@ |
|||||
# 테라클론 공장초기화 계약 |
|
||||
|
|
||||
웹 `POST /api/v1/system/factory-reset`과 SW2 모니터는 동일한 `Store.FactoryReset`을 호출한다. |
|
||||
초기화 후 기본 계정은 `terauser / terauser` 하나다. 고객 계정과 사용자 정의 그룹, 로그인 세션/토큰/실패 기록, 주소 필터, 장비 설정과 보류 변경, 저장 인증정보, 고객 TLS 키와 인증서, 과거 로그와 내보내기, 진단 파일, 알림 전송/재시도 기록, 업로드된 펌웨어 패키지를 삭제한다. TLS 기본 키는 새로 생성한다. |
|
||||
|
|
||||
설정 저장을 마친 후 기존 시스템 제어 서비스에 재부팅을 요청한다. 설치된 펌웨어와 제조사 업데이트 검증 키는 유지한다. 공장초기화 자체 및 다음 부팅에서 새로 발생한 시스템/감사 이벤트는 새 기록이다. 파일시스템 상의 삭제이며 저장 매체의 포렌식 보안 소거 기능은 아니다. |
|
||||
|
|
||||
## 구현 |
|
||||
|
|
||||
- frontend 서버는 SQLite를 열지 않고 사용자·장비 데이터를 tera-api로 전달한다. 기존 로컬 계정 API와 터미널용 legacy 경로는 서버에 등록하지 않는다. |
|
||||
- 이전 설치의 `/var/lib/teraweb/frontend.db` 및 `/var/lib/tera-clone-demo-web/teraclone.db`와 `-wal`, `-shm`, `-journal`은 공통 초기화에서 정리한다. |
|
||||
- 인증 파일 로딩은 초기 계정 목록에 병합하지 않고 저장된 목록으로 교체한다. 재기동 시 admin 등 샘플 계정이 재생성되지 않는다. |
|
||||
- 일반 로그 삭제의 감사 로그 보호 정책과 공장초기화는 별도다. `preserve_on_factory_reset`은 false이며 true 설정은 거부한다. |
|
||||
- reset은 진행 중인 HTTP 변경을 기다리고, 완료 후 재부팅 전 추가 변경을 거부한다. 진단·알림 작업을 종료한 뒤 결과 파일과 재시도 상태를 지운다. |
|
||||
- 네트워크 기본 설정은 재부팅 시 적용되도록 저장한다. 기존 rollback snapshot/timer가 기본값을 다시 덮어쓰지 못하도록 제거한다. |
|
||||
- 초기화 중 삭제/저장/서비스 오류가 있으면 성공을 반환하지 않는다. 펌웨어 업그레이드 진행 중에는 초기화를 거부한다. 저장 장치 오류 시 이미 삭제한 데이터는 자동 복구되지 않는다. |
|
||||
|
|
||||
## 함께 배포할 구성 |
|
||||
|
|
||||
`build/factory-reset-linux-arm64/`에 다음 ARM64 실행 파일을 빌드했다. |
|
||||
|
|
||||
- tera-api (테라클론 API 설치 이름은 tera-clone-demo-api) |
|
||||
- tera-clone-demo-web |
|
||||
- tera-logd, tera-netd, tera-alarmd, tera-updated, tera-snmpd |
|
||||
|
|
||||
API만 교체하면 이전 보조 서비스가 새로운 factory_reset 명령을 지원하지 않을 수 있으므로 함께 교체한다. clone 전용 alarmd 인스턴스에도 같은 alarmd 빌드를 사용한다. 웹 env의 TERACLONE_DB_PATH는 제거했다. |
|
||||
|
|
||||
80 포트의 별도 teraWeb/8082 API가 계속 설치되어 있으면 해당 서비스에도 동일 소스의 변경을 통합해야 한다. 수정 대상은 tera_Clone_Demo 소스다. 2026-09-08 해당 웹/API와 보조 서비스 8개를 장비에 배포했다. 기존 별도 tera-web/tera-api 실행 파일은 교체하지 않았다. 장비의 수동 배포 백업 디렉터리나 다른 프로그램의 파일을 임의로 재귀 삭제하지 않는다. |
|
||||
|
|
||||
## 검증 |
|
||||
|
|
||||
- 임시 저장소에 계정·세션·그룹·로그·비밀정보·고객 인증서·진단 파일을 넣고 초기화한 후 재개방: terauser/terauser만 로그인 성공, 이전 세션/계정 거부. |
|
||||
- 로그 네 종류(audit/system/port/link): 로그 서비스 소켓을 통한 삭제, 디스크 원문 제거, 재개방 후 빈 기록 검증. |
|
||||
- 네트워크 pending transaction/rollback 제거 및 재부팅용 파일 저장 검증. |
|
||||
- 알림 전송 기록과 재시도 상태: 재기동 후 복구되지 않음 검증. |
|
||||
- 업로드된 서명 펌웨어 패키지 삭제 및 현재 설치 버전 유지 검증. |
|
||||
- SQLite sidecar 삭제, 비관련 파일 보존, stateless frontend의 로컬 계정 생성 거부 검증. |
|
||||
- API 권한, reset 완료 후 쓰기 차단, SW2 모니터 호출 경로, 한국어/영어 안내와 프론트엔드 회귀 테스트 통과. |
|
||||
- ARM64 실행 파일 7개 빌드 완료. 실장비 서비스 배포·재시작 및 HTTPS/해시 검증을 완료했다. 공장초기화와 장비 재부팅 자체는 실행하지 않았다. |
|
||||
|
|
||||
배포 백업: `/userdata/tera-backups/factory-reset-deploy-20260908-104450`. 배포 중 발견된 기존 updater 상태 파일의 `apply_policy` 호환성을 수정하고 회귀 테스트를 추가했다. |
|
||||
|
|
||||
## Factory defaults correction (2026-09-08) |
|
||||
|
|
||||
CAPTCHA defaults to enabled, including after factory reset and state reload. The 48 serial ports default to 9600 8N1; the COM5 management console stays at 115200. On-device checking after setting 9600 confirmed CR response on CH18, the only connected port. Login autofill uses username/current-password. |
|
||||
|
|
||||
## PC clock synchronization (2026-09-08) |
|
||||
|
|
||||
The first successful browser login with time.sync permission after API service startup submits PC UTC time through the existing tera-timed boundary. Synchronization precedes session issuance and retains the configured device timezone and NTP settings. Later logins do not override the clock again. Failed synchronization is audited and retried at the next authorized login. Browser access is required; no PC clock is available to the device while the PC/browser is disconnected. No public unauthenticated clock setter is exposed. |
|
||||
@ -1,68 +0,0 @@ |
|||||
# tera-logd 의사결정 질문서 구현 대조 |
|
||||
|
|
||||
**후속 사용자 결정 반영:** 원문 저장 유지(D-01), 관리자 보존기간 변경 허용(D-02), 포트 운영자에게 허용 포트 조회·다운로드만 허용(D-10), 공장초기화 전체 삭제 유지(D-11)가 승인됐습니다. 아래 표는 결정 이전의 질문서 대비 차이 기록이며, 현재 웹 구현은 WEB_LOG_FEATURES.md를 참조하세요. |
|
||||
|
|
||||
2026-09-08 · 대상: 2026-07-24 질문서와 현재 tera_Clone_Demo 소스 |
|
||||
|
|
||||
**판정: 질문서의 권고 기준을 모두 충족하지 않는다.** 질문서에는 선택·승인란이 비어 있다. 권고안을 확정 요구사항으로 간주하지 않았으며, 실제 코드에서 확인한 일치·차이와 시험 증거 부족을 구분했다. 첨부 원문은 수정하지 않았다. |
|
||||
|
|
||||
공장초기화는 이후 사용자의 명시 지시인 **기존 데이터 삭제, 기본 계정 terauser/terauser만 재생성**이 우선한다. 질문서 D-11의 로그 보존을 되살리면 최신 지시를 위반한다. 초기화·부팅 이후 새로 발생하는 감사 로그는 생성된다. |
|
||||
|
|
||||
## 우선 확인할 차이 |
|
||||
|
|
||||
1. **원문 저장(D-01)**: 시리얼 RX/TX payload를 base64로 실제 저장한다. metadata-only 권고와 다르며, base64는 암호화가 아니다. `backend/cmd/tera-seriald/main.go:267`의 `recordPayload`, `backend/internal/adapters/mock/store.go`의 `RawUARTPayloadCapture: true`로 확인했다. |
|
||||
2. **365일 보장(D-02, D-13)**: 기본값은 365일이나 API가 1~365일 변경을 허용한다. 용량 부족 시 system/port의 오래된 세그먼트를 기간 만료 전에도 회수할 수 있다. `logstorage.ensureCapacityLocked`, `reclaimOldestLocked`, `validateRetentionDays`가 근거다. 기본값 365일은 최소 365일 보장과 다르다. |
|
||||
3. **쓰기 정책(D-05)**: `logstorage.Store.Append`는 각 레코드에 `file.Sync()`를 호출한다. 질문서의 일반 레코드 배치 원칙과 다르다. 또한 `recordAuditEventLocked`가 `persistAuditLogLocked`의 오류를 무시하므로 모든 변경 명령에 durable audit 성공을 보장한다고 할 수 없다. |
|
||||
4. **검색 인덱스(D-14)**: 실제 인덱스는 `index/log-index-v1.json`이다. 재구축 가능한 인덱스와 checksummed segment는 구현됐으나 질문서의 SQLite WAL 구조와 다르다. |
|
||||
5. **납품 검증(D-21~23)**: 이번 확인 범위에는 승인된 48채널 장기 부하·실제 전원 차단·eMMC 수명 측정 결과가 없다. 호스트 테스트 통과로 이를 대신할 수 없다. |
|
||||
|
|
||||
## 23개 항목 대조 |
|
||||
|
|
||||
| ID | 항목 | 판정 및 확인 내용 | |
|
||||
|---|---|---| |
|
||||
| D-01 | Port Log 원문 | 불일치. RX/TX 원문 저장 기능이 켜진 기본 프로파일과 payload 저장 경로가 있다. metadata-only로 바꿀지는 별도 제품 결정 사항이다. | |
|
||||
| D-02 | 365일 보존 | 불일치. 기본 365일, 설정 범위 1~365일. system/port 용량 회수는 보호기간 고정 보장이 아니다. | |
|
||||
| D-03 | 이벤트율·집계 | 부분 구현. seriald 보고 주기 기본 10초가 있으나 payload는 별도 이벤트로 기록한다. 승인된 events/sec·48채널 용량 산정은 확인하지 못했다. | |
|
||||
| D-04 | 저장 암호화·삭제 | 권고의 제한적 logical wipe 방향과 일부 일치. 로그 JSONL은 평문이며 파일 삭제가 forensic erase를 보장하지 않는다. 보안 책임자 승인 기록은 없다. | |
|
||||
| D-05 | 전원 장애 손실 | 차이 있음. 레코드마다 Sync하며 일반 batching이 아니다. audit 저장 실패를 호출자에게 강제 전파하지 않는 경로가 있다. 실제 power-cut 손실 한계는 미검증이다. | |
|
||||
| D-06 | 분류·schema | 부분 구현. audit/system/port/link, version/sequence/UTC/checksum과 크기 제한 존재. 원문 필드가 포함되며 최종 승인 catalog·migration 계약은 미확인이다. | |
|
||||
| D-07 | 포트별 로그 설정 | 부분 구현. per-port/bulk Read/Send 및 capture 설정과 세션·감사 기록 경로가 존재한다. 모든 설정 조합에서 필수 감사 이벤트가 유지되는지는 별도 시험이 필요하다. | |
|
||||
| D-08 | keyword | 부분 구현. 대소문자를 무시한 literal match와 rule ID가 있다. 원문 저장이 별도로 존재하여 metadata-only 저장 목표를 충족하지 않는다. 청크 경계·장기 부하 기준은 미검증이다. | |
|
||||
| D-09 | 시각·보정 | 부분 구현 및 이번 개선. UTC와 클래스별 증가 sequence가 있다. 첫 time.sync 권한 사용자 로그인 때 PC 시간 자동 적용, 변경 전후 감사 메시지, 이후 세션 발급을 추가했다. trusted-time retention 및 역행·미동기화 전체 정책은 별도 검증 대상이다. | |
|
||||
| D-10 | 권한 | 부분 구현. log.export 등 서버 권한 검사가 존재한다. system/port clear 기능도 있으므로 질문서의 wipe 전용 역할·일반 관리자 삭제 금지 기준과 별도 대조가 필요하다. | |
|
||||
| D-11 | 공장초기화 | 최신 지시에 따라 의도적으로 다름. 예전 로그·인덱스 등 초기화 대상 데이터를 삭제하고 기본 계정만 재생성한다. 질문서 로그 보존은 현재 기준으로 채택하지 않는다. | |
|
||||
| D-12 | cap·floor | 구현 존재. logd가 공통 capacity policy의 클래스 cap과 free-space floor를 사용한다. 실제 이벤트율·365일·업데이트 동시 사용 용량 보장 근거는 부족하다. | |
|
||||
| D-13 | pressure·회수 | 불일치 부분 존재. cap/floor와 오류는 있으나 system/port의 보호기간 이전 회수 경로 및 audit 오류 무시 경로가 있어 권고한 보존·fail-closed 보장과 다르다. | |
|
||||
| D-14 | segment·index | 일부 일치. 4 MiB 기본 segment, 체크섬, 회전, 잘린 active tail 복구, 인덱스 재구축 테스트 존재. SQLite WAL 대신 JSON index를 사용한다. | |
|
||||
| D-15 | IPC·실패 처리 | 부분 구현. 제한 크기의 Unix socket request/response와 timeout 존재. producer 전체의 durable ack·queue overflow·drop metric 보장은 확인하지 못했고 audit 오류 전파 차이가 있다. | |
|
||||
| D-16 | 검색·pagination | 부분 구현. sequence 기반 page와 sparse checkpoint 존재. API/UI 필터 존재. 승인된 365일 데이터셋 검색 SLA·전체 범위 완전성 증거는 부족하다. | |
|
||||
| D-17 | export | 부분 구현. 제한된 행 수·필터와 NDJSON 출력 경로가 있다. 먼저 행 배열을 구성하므로 완전한 저장소 직결 streaming은 아니다. 질문서의 manifest/checksum 패키지 보장은 확인하지 못했다. 원문 제외 기준도 충족하지 않는다. | |
|
||||
| D-18 | Remote Syslog | 부분 구현. UDP/TCP 대상과 alarm delivery 경로가 있다. 현재 대상 검증은 TLS protocol을 지원하지 않는다. 승인된 전송 범위·장애/중복 시험 결과는 별도 확인해야 한다. | |
|
||||
| D-19 | 무결성 | 일부 일치. SHA-256 checksum 및 증가 sequence, 손상 검출이 있다. root 공격에 대한 서명·HMAC·외부 anchor/rollback 보장은 확인하지 못했다. | |
|
||||
| D-20 | packaging·health | 구현 및 일부 실기 확인. 별도 tera-logd 서비스·socket·기동 스크립트, 장비 health 응답 존재. 전체 펌웨어 upgrade/rollback matrix 시험은 미검증이다. | |
|
||||
| D-21 | eMMC 수명 | 미검증. 이번 확인에서 승인된 write/day·write amplification·PRE_EOL/LIFE_TIME 결과를 확보하지 못했다. | |
|
||||
| D-22 | SLA·soak | 미검증. CH18 1포트 CR 응답 확인은 48채널 장기 동시 부하 시험을 대체하지 않는다. | |
|
||||
| D-23 | 납품 증거 | 부분 확보. 호스트 테스트·이번 두 실행파일 해시·실기 설정 확인은 있다. 질문서가 요구하는 전원 차단·장기 soak·전체 traceability 승인 묶음은 없다. | |
|
||||
|
|
||||
## 주요 코드 근거 |
|
||||
|
|
||||
- 원문 수집: `backend/cmd/tera-seriald/main.go`의 `reportStreamEvents`, `recordPayload`; `backend/internal/serial/stream.go` |
|
||||
- 보존·감사·export: `backend/internal/adapters/mock/store.go`의 `validateRetentionDays`, `recordAuditEventLocked`, `StreamLogExport` |
|
||||
- 저장·복구: `backend/internal/logstorage/store.go`의 `Append`, `ensureCapacityLocked`, `writeIndexLocked`, `inspectSegment` |
|
||||
- IPC: `backend/internal/logservice/socket.go` |
|
||||
- 용량: `backend/internal/storage/capacity/governor.go`, `backend/cmd/tera-logd/main.go` |
|
||||
- Syslog: `backend/internal/alarmdelivery/controller.go`의 `normalizeSyslogTarget` |
|
||||
- 초기화: `backend/internal/adapters/mock/factory_reset.go`, `backend/internal/logstorage/factory_reset.go` |
|
||||
- 이번 시간 변경: `backend/internal/adapters/mock/store.go`의 `Login`, `backend/internal/adapters/mock/login_time_test.go` |
|
||||
|
|
||||
## 이번 변경과 확인 범위 |
|
||||
|
|
||||
- 주소 필터 IPv6 탭·추가 화면 제거. 기존 IPv6 URL은 IPv4 화면으로 이동. 네트워크 인터페이스 자체의 IPv6 설정·기존 API 필터 엔진은 변경하지 않았다. |
|
||||
- 첫 관리자 브라우저 로그인 시 PC 시간 동기화. 엄밀히는 API 프로세스 시작 후 최초 1회이며 API 서비스 재시작 후에도 적용한다. 로그인 실패·time.sync 권한 없는 사용자는 시간을 변경하지 못한다. 실패 시 로그인을 막지 않고 다음 관리자 로그인에서 재시도한다. |
|
||||
- PC/브라우저 미접속 상태에서는 PC 시각을 얻을 수 없다. 기존 장비 시간대·NTP 설정은 유지한다. |
|
||||
- 호스트 테스트: API, mock store, systemtime/timesocket, logstorage/logservice, frontend handlers/service 통과. |
|
||||
- 실기: 성공한 관리자 로그인으로 1970년 장비 시각이 현재 PC UTC 시각으로 변경됨; 세션 유지; IPv6 탭 미노출·이전 URL redirect 확인; 12개 서비스 health ok; CH18 CR up 확인. |
|
||||
- 실기 재부팅·공장초기화·전원 차단은 이번 확인에서 실행하지 않았다. 자동 시간 동기화의 새 프로세스 경로를 확인했으며 초기화 상태는 기존 호스트 테스트로 검증했다. |
|
||||
- 배포 API SHA-256: e05b45136b5ea6765289a6aedd6ec6752aca7169329d2e0bf35dd228183ee540 |
|
||||
- 배포 Web SHA-256: ace1cada6dc20f38ae91f19b235d72265ed0f2cb1980990cd404149333bf00d6 |
|
||||
- release/latest의 두 바이너리도 같은 해시로 갱신했다. 질문서 평가를 이유로 로그 원문·보존 정책을 이번에 임의 변경하지 않았다. |
|
||||
@ -1,23 +0,0 @@ |
|||||
# 웹 로그 기능 적용 기록 |
|
||||
|
|
||||
2026-09-08 · release 20260908-log-web-2 |
|
||||
|
|
||||
사용자 승인: 명령·응답 원문 저장 유지, 보존기간 관리자 변경 허용, 포트 운영자는 자신에게 허용된 포트만 조회·다운로드, 초기화는 기존 데이터 전체 삭제. |
|
||||
|
|
||||
| 항목 | 적용 내용 | |
|
||||
|---|---| |
|
||||
| 1 원문 로그 | 기존 RX/TX 원문·명령·응답 기록 및 표시 유지. 검색에 payload 원문도 포함. | |
|
||||
| 2 보존기간 | 관리자 로그 화면에 시스템·감사·포트 보존일 설정 추가. 서버 허용 범위 1~365일, 기본 365일. 적용과 영구 저장 수행. 기간 축소·용량 회수에 따른 삭제 가능성을 표시. | |
|
||||
| 3 포트별 기록 | 기존 수신/송신·표시 형식·일괄 적용 기능 유지. savePort가 선택된 포트를 bulk API로 전달. | |
|
||||
| 4 키워드 알림 | 기존 포트 고급 설정의 키워드와 알림 설정의 수신 키워드 감지를 유지. 문자열·대소문자 무시 방식 안내 추가. 알림 이벤트는 기존 포트 로그에 표시. | |
|
||||
| 5 검색·목록 | 기존 기간·포트·사용자·방향·유형·검색어 유지. 50/100/200/500건 페이지 단위 조회와 이전/다음 버튼 추가. | |
|
||||
| 6 다운로드 | CSV, JSON 배열, JSONL 지원. UI 100/500/1000/5000건 상한 선택, 서버 최대 10000건 유지. 필터를 서버에 전달. CSV 인용부호·쉼표·줄바꿈 처리, 수식 주입 방지, UTF-8 BOM, no-store 응답. | |
|
||||
| 7 권한 | 포트 운영자 포트 로그 메뉴 허용. API에서 허용 포트만 pagination·export 이전 필터링. 명시적으로 거부된 포트 요청은 403. 시스템 로그·삭제·보존 설정 변경은 금지. 기존 그룹+사용자별 유효 권한 계산 유지. | |
|
||||
| 8 저장 공간 | 실제 logd snapshot 기반 클래스별 사용량/상한·남은 공간·경고·최고령 기록 표시. 30초 순증가량에서 클래스 상한 도달까지의 추가 저장 가능 일수 추정. 데이터가 없거나 증가하지 않으면 추정 불가로 표시. 이는 보존기간 보장이 아님. | |
|
||||
| 9 원격 Syslog | 기존 서버·포트·UDP/TCP 설정에 전송할 이벤트 분류 선택 추가. 최근 관측한 전송의 대기/성공/실패·시도 횟수·오류 상태를 조회. API 재시작 전 이력 전체 목록은 제공하지 않음. | |
|
||||
|
|
||||
CAPTCHA는 4자리이며 기본 활성화·2분 만료·1회 사용·접속자 결합 검증을 유지한다. 대시보드 저장장치 사용량의 “로그·백업 등이 함께 사용하는 데이터 파티션” 문구를 제거했다. |
|
||||
|
|
||||
검증: authcaptcha/API/mock/프론트엔드 handlers·service 테스트 통과. 권한 테스트는 읽기 허용 CH18, 명시 거부 CH01, 시스템 로그·삭제·retention 차단, CSV/JSON 원문 보존과 다른 포트 누출 방지를 확인한다. 실제 장비에서 4자리 CAPTCHA 로그인, 저장 공간/보존 설정/Syslog 상태 GET, CSV·JSON·JSONL 다운로드 및 파싱을 확인했다. 원격 수신 서버로의 전송 시험, 48채널 장기 부하, 전원 차단·공장초기화는 이번에 실행하지 않았다. 대화에서 보류한 커널/드라이버 문제는 해결됐다고 주장하지 않는다. |
|
||||
|
|
||||
장비 배포 파일과 release/latest/rootfs/usr/bin의 2개 파일 SHA-256을 일치시켰다. 모든 변경은 기존 tera-clone-demo 서비스 대상이며 legacy tera-api/tera-web는 교체하지 않았다. |
|
||||
@ -1,27 +0,0 @@ |
|||||
# 내부 브라우저 CPU 비교 |
|
||||
|
|
||||
장비의 동일한 matrix-browser 옵션(-no-sandbox --disable-gpu), 동일한 백엔드/CR 설정을 유지하고 표시 콘텐츠만 바꿨다. 아래 수치는 vmstat 1의 첫 누적 행을 제외한 구간별 user+system이다. 짧은 비교 실험이며 장기 평균은 아니다. |
|
||||
|
|
||||
| 표시 콘텐츠 | 전체 CPU 사용 | |
|
||||
| --- | --- | |
|
||||
| 기존 장비 UI | 41~42% | |
|
||||
| about:blank | 4~5% (반올림된 idle 기준 약 4~6%) | |
|
||||
| 원래 URL로 재실행 | 41~44% | |
|
||||
| 현재 로그인 HTML/CSS 복사본, JavaScript 제외, 애니메이션 유지 | 43~47% | |
|
||||
| 같은 복사본, CSS animation/transition만 비활성화 | 4~9% | |
|
||||
|
|
||||
로그인 CSS의 login-progress-slide는 무한 반복 애니메이션이다. JS를 제거한 비교 화면에서 이 애니메이션을 포함한 CSS 동작만으로 높은 부하가 재현됐다. 원래 실행 페이지의 JS 상태나 실제 DOM을 직접 확인한 것은 아니므로 원래 페이지가 왜 계속 애니메이션 중인지까지 확정하지 않는다. |
|
||||
|
|
||||
웹에서 가능한 개선은 로그인 준비 막대의 무한 이동 효과를 제거하고 실제 진행률 변경만 표시하는 것이다. 상태 조회/인증/CAPTCHA/CR 검사를 끄는 변경은 필요하지 않다. login.js가 AbortSignal.timeout을 직접 사용하므로 장비 Qt 브라우저의 지원 여부 확인과 구형 브라우저용 타임아웃 대체 구현도 검토해야 한다. 이 API의 미지원은 아직 런타임에서 확인하지 않았다. |
|
||||
|
|
||||
실험 후 /etc/init.d/S99matrix-browser start로 원래 URL을 복구했다. 영구 설정과 배포 파일은 바꾸지 않았다. 이 결과는 별도로 확인된 RCU stall/IRQ95 차단의 해결을 의미하지 않는다. |
|
||||
|
|
||||
원시 기록: ../runtime/browser-blank-comparison.log 및 ../runtime/browser-animation-comparison.log. |
|
||||
|
|
||||
## 수정 및 장비 배포 확인 |
|
||||
|
|
||||
로그인 준비 막대의 무한 애니메이션과 관련 keyframes를 제거하고 CSS 캐시 버전을 갱신했다. 준비 상태에 따른 막대 폭 및 메시지 갱신 코드는 유지했다. 프런트엔드 핸들러 테스트 및 ARM64 빌드가 통과했다. |
|
||||
|
|
||||
웹만 배포했고 해시 `1d6b8ba457e702a70fab9b03156d80a012ead37440fc8d93e5914ddd9c70c3f6`, HTTP80, 준비 상태12/12, CAPTCHA true를 확인했다. 내부 브라우저를 원래 URL로 재실행한 후 실측 CPU는 첫 누적 행 제외 11~14% 구간이었다. 수정 전 동일 URL의 41~44%보다 낮다. CR 서비스는 변경하지 않았다. 커널 IRQ 장애 해결이나 장시간/재부팅 후 성능 보장을 의미하지 않는다. |
|
||||
|
|
||||
배포 기록: ../runtime/static-progress-transfer.log. CPU 측정: ../runtime/static-progress-cpu.log. |
|
||||
@ -1,75 +0,0 @@ |
|||||
# 교체 장비 UI 지연 진단 — 2026-09-09 |
|
||||
|
|
||||
대상: 192.168.0.223, HTTP 80, COM4, 커널 5.10.160 #23. 사용자가 보고한 화면은 PC 브라우저이다. |
|
||||
|
|
||||
## 확인 결과 |
|
||||
|
|
||||
1. 초기 설정은 11:35:45에 완료됐다(`runtime synchronization completed elapsed_ms=35358`). |
|
||||
2. 이후 11:55:19부터 11:58:17까지 시리얼 상태 조회, 로그 기록 및 저장 공간 조회에서 응답 실패가 발생했다. API 로그에 degraded 메시지가 10개 있었다. 11:58:36에는 시리얼 재조정 복구가 기록됐다. |
|
||||
3. 커널은 부팅 2700.237766초에 CPU0 RCU stall, 2832.568711초에 `irq 95: nobody cared`, 2832.732864초에 `Disabling IRQ #95`를 기록했다. IRQ95 핸들러는 `wch_interrupt`이고 /proc/interrupts의 카운터는 1500000이었다. API 응답 실패와 시간대가 겹친다. |
|
||||
4. 이 장비의 stall 스택에는 네트워크 수신/softirq 및 readlink 경로가 포함된다. task 이름이 tera-seriald라는 사실만으로 사용자 공간 시리얼 코드가 직접 원인이라고 단정하지 않는다. 이전 장비의 #22 스택과도 구분해야 한다. |
|
||||
5. 내부 브라우저는 `matrix-browser -no-sandbox --disable-gpu 127.0.0.1`로 실행된다. matrix-browser와 렌더러를 몇 초간 SIGSTOP한 비교에서, vmstat의 구간별 전체 CPU 사용량은 약 43%에서 12~16%로 감소했다. SIGCONT와 자동 복구용 watchdog으로 원상 복구했고 두 프로세스의 실행 상태를 확인했다. 서비스 시작 설정은 변경하지 않았다. |
|
||||
6. 장애 기록 이후 시점의 설정 읽기 및 두 시리얼 상태 요청을 포함한 진단 helper는 52ms에 완료됐다. 이 수치는 장애가 발생한 시간대의 API 지연이나 로그인된 PC 화면의 응답 시간을 대체하지 않는다. |
|
||||
|
|
||||
## 판단과 한계 |
|
||||
|
|
||||
- 설정 완료 이후에도 운영 중 장비 측 정체가 발생한다. 로그인 대기 연장으로 이후 장애를 예방할 수 없다. |
|
||||
- 내부 브라우저의 지속적인 CPU 부하는 별도로 확인됐으나, 이것만으로 PC 화면 지연의 단독 원인이라고 단정할 수 없다. |
|
||||
- API의 복구 메시지는 WCH IRQ 또는 실제 시리얼 송수신 복구를 보장하지 않는다. |
|
||||
- 커널/드라이버의 최초 장애 유발 조건과 개별 PC 메뉴의 지연 시간은 아직 확정하지 않았다. 로그인된 진단 브라우저가 필요하다. |
|
||||
- 이번 확인에서는 커널, IRQ 설정, HTTP 포트, 9600bps 설정, 배포 파일을 변경하지 않았다. |
|
||||
|
|
||||
## 후속 확인 |
|
||||
|
|
||||
- 로그인된 PC 화면에서 메뉴별 응답 및 표시 지연을 기록하고 장비 로그와 비교한다. |
|
||||
- 커널 담당자에게 #23의 RCU stall 및 WCH IRQ95 차단 로그를 전달해 인터럽트 처리 원인을 확인한다. |
|
||||
- 내부 화면이 필요한 운영 조건을 유지하면서 불필요한 렌더링 부하를 줄인다. |
|
||||
|
|
||||
## 로그인 후 PC 브라우저 확인 |
|
||||
|
|
||||
사용자가 진단 브라우저에 로그인한 뒤, 설정을 적용하거나 저장하지 않고 메뉴를 전환했다. 시간은 자동 클릭 시작부터 접근성 화면 확인까지의 관측 시간이며, 브라우저 자동화 왕복/관측 비용을 포함한다. API 단독 소요 시간이나 정밀 렌더링 프로파일은 아니다. |
|
||||
|
|
||||
| 화면 | 관측 시간 | |
|
||||
| --- | --- | |
|
||||
| 네트워크 설정 | 390ms, 재확인 251ms | |
|
||||
| 포트 설정 | 388ms, 재확인 350ms | |
|
||||
| 포트 상태 | 447ms, 재확인 384ms | |
|
||||
| 대시보드 | 336ms, 재확인 318ms | |
|
||||
| 시스템 로그 | 100건 표시 확인 813ms | |
|
||||
|
|
||||
확인 중 수집된 브라우저 error/warn 로그는 없었고, 수초 단위의 심한 지연은 재현하지 못했다. 포트 설정 화면의 9600bps도 확인했다. 진단 후 대시보드로 돌아왔다. 부팅 직후나 앞서 기록된 장비 장애 중의 동작은 이 결과로 검증되지 않는다. 현재의 정상 전환을 근거로 장애 해결을 선언하지 않는다. |
|
||||
|
|
||||
## 사용자 승인 재부팅 후 확인 (12:17경 재부팅) |
|
||||
|
|
||||
- COM4에서 정상 종료 및 새 커널 부팅, 로그인 프롬프트를 수집했다. 원시 기록은 `../runtime/reboot-ui-20260909.log`에 있다. |
|
||||
- 사용자의 콘솔 로그인 후 12:22:47(가동 약 5분)에 vmstat를 수집했다. 구간별 user+system은 52%, 62%, iowait는 16%, 13%였다. 이 시점은 전원 인가 직후 수십 초와는 구분한다. |
|
||||
- `/api/v1/auth/startup` 응답은 ready=true, 12/12, elapsed_ms=327015였다. 로그인 화면도 준비 시간 327초를 표시했다. |
|
||||
- 현재 부팅의 dmesg 검색에서는 RCU stall/IRQ 차단 메시지를 찾지 못했다. 이전 부팅에서 발생한 장애를 이번 부팅에 발생했다고 간주하지 않는다. |
|
||||
- uptime 409.27초에서 411.32초 사이 IRQ95는 398728에서 400840으로 증가했다(약 1030회/초). IRQ99는 1772에서 1782로 증가했다. 발생 횟수만으로 IRQ 장애라고 판정하지 않는다. |
|
||||
- tera-api의 stdout/stderr는 /dev/null이다. 완료된 startup 응답에는 단계별 소요 시간이 없어, 이번 준비 327초의 지배 단계나 PC 시간 동기화 대기 비중을 사후 확정할 수 없다. |
|
||||
- 기존 진단 탭은 종료되어 사용자 로그인 후 UI 측정이 이어지지 않았다. 새 진단 탭을 열었고 로그인 입력을 요청했다. 이 구간의 로그인 후 메뉴 지연을 측정했다고 보고하면 안 된다. |
|
||||
|
|
||||
다음 부팅 검증에는 부팅 전에 단조 시간 기준의 단계별 준비 상태·CPU·API 소요 시간 수집을 마련해야 한다. 현재 결과로 로그인 준비 단계 누락 또는 커널 장애 하나를 원인으로 확정하지 않는다. |
|
||||
|
|
||||
## 이번 부팅의 로그인 후 지연 재현 (12:25경, uptime 약 8분) |
|
||||
|
|
||||
- 새 진단 탭 로그인 후 메뉴 전환을 실행했다. 클릭 후 첫 접근성 관측(약 250~350ms)은 이전 화면을 표시했다. 따라서 이 관측 시간을 화면 전환 완료 시간으로 사용하면 안 된다. |
|
||||
- 포트 설정 이동에서 URL 변경과 화면을 기다린 관측은 2773ms였다. 그 시점에는 9600 설정값이 아직 표시되지 않았고, 다음 관측에서는 표시됐다. 별도 네트워크 메뉴 전환 대기도 시간 초과 후 다음 관측에서 완료돼 지연이 확인됐다. |
|
||||
- integration.js의 navigateWithinConsole는 navigating=true이면 새 요청을 반환해 무시하고, initializeCurrentPage의 데이터 로딩을 기다린 다음에야 navigating=false로 바꾼다. 연속 메뉴 클릭이 반영되지 않는 체감에 기여하는 구현이다. HTML/API/브라우저 각각의 지배적 지연 시간은 아직 분리하지 못했다. |
|
||||
- 이 시점 vmstat는 user+system 약 54%, iowait 0%였다. dmesg에는 새 RCU stall/IRQ 차단 검색 결과가 없었다. 앞선 iowait 13~16%가 계속 유지된다고 보고하지 않는다. |
|
||||
- 장비 내부 loopback에서 비인증 로그인 HTML을 wget으로 읽는 시간은 7ms, 10ms, 6ms였다. 인증된 화면/API 및 PC 네트워크의 지연을 이 측정으로 배제하지 않는다. |
|
||||
- 브라우저 error/warn 로그는 없었다. 설정 변경이나 커널 수정은 하지 않았다. |
|
||||
|
|
||||
## 적용 수정 |
|
||||
|
|
||||
- 메뉴 초기화 중 마지막 이동 요청을 보존한다. 초기화를 겹쳐 실행하지 않으므로 이전 화면 요청이 새 폼에 값을 기록하는 경쟁을 피한다. 인증 만료 시 이동 대기는 폐기한다. |
|
||||
- 포트 설정 화면은 `GET /api/v1/ports?limit=48&runtime=false`로 저장된 실행 설정을 먼저 읽는다. 이 선택적 조회에서는 시리얼/세션 런타임 수집을 호출하지 않는다. 권한 검사와 포트별 필터는 동일하며, 기본 API 응답은 기존 실시간 동작을 유지한다. |
|
||||
- 선택적 설정 응답은 미관측 링크를 unknown으로 취급한다. 설정 폼을 채운 뒤 기존 런타임 폴링을 별도로 시작한다. PATCH/적용·로그인 준비 판정·9600bps·HTTP80 설정은 수정하지 않는다. |
|
||||
- 검증: JavaScript 49개 통과, backend internal/api·adapters/mock·startup 통과, frontend internal/handlers·service 통과. ARM64 API/웹 빌드 성공. |
|
||||
- 이 수정은 포트 설정의 런타임 대기와 메뉴 클릭 무시를 줄이는 범위다. 모든 메뉴의 장비 측 지연 또는 커널 장애가 해결됐다는 뜻은 아니다. |
|
||||
|
|
||||
## 배포 시도 및 복구 |
|
||||
|
|
||||
API/웹 두 파일만 교체하는 배포를 시도했다. 12:36:28 백업 위치는 `/userdata/tera-backups/config-read-20260909-123628`이다. 준비 상태가 7/12에서 지연돼 자동 복구했고 12:40:39 복구 완료 로그를 확인했다. 이 과정에서 새 부팅의 커널에 uptime 1308초 RCU stall, 1411.077981초 `Disabling IRQ #95`, GPU dvfs timeout이 기록됐다. 수정된 설정 조회가 이 장애를 촉발했다는 증거는 없으며, 기존 부팅에서도 같은 유형의 IRQ 차단이 있었다. |
|
||||
|
|
||||
복구 후 API SHA256은 `ef466f00788868617d8528a10ee597e2119ca6965eb81ed5e5e89bfc8a87536f`, 웹은 `2b7001dd33ad07c9a1886712926384e065c0c09c0715873f30b107969bc23eb9`로 이전 파일과 일치했다. 준비 상태는 12/12, 8383ms, CAPTCHA true였다. 이는 IRQ 또는 실물 포트 송수신 복구의 증거가 아니다. 수정 빌드는 `build/linux-arm64`에 있으며 장비에는 적용되어 있지 않다. 전송 및 커널 원시 기록은 `../runtime/config-read-transfer.log`에 있다. |
|
||||
@ -1,61 +0,0 @@ |
|||||
# 부팅 초기화 시간 확인 |
|
||||
|
|
||||
## 로그인 준비 상태 |
|
||||
|
|
||||
`GET /api/v1/auth/startup`은 인증 없이 준비 단계와 경과 시간을 제공합니다. 로그인 화면의 `/login/backend-status`는 이 상태가 `ready`인 경우에만 로그인 정책을 조회하고 로그인을 허용합니다. API의 로그인·CAPTCHA POST도 초기화 중에는 503으로 차단합니다. |
|
||||
|
|
||||
설정 복원, 인증서, SNMP, 접속 설정, 시리얼 설정, 활성화된 포트 서비스 설정이 성공해야 합니다. 마지막으로 48개 시리얼 채널의 가용성과 운영 환경의 세션 서비스 정상 응답을 확인합니다. 부팅 알람 전송은 필수 성공 조건이 아닙니다. |
|
||||
|
|
||||
게이지는 완료 단계 수/전체 단계 수이며 예상 남은 시간 비율이 아닙니다. 표시되는 경과 시간은 API 프로세스 시작 기준입니다. API가 응답하기 전에는 단계와 시간을 알 수 없어 대기 표시를 사용합니다. 준비 완료 후 시간은 고정됩니다. |
|
||||
|
|
||||
필수 단계가 오류를 반환하면 3초 간격으로 실제 재시도합니다. 25초 이상 완료되지 않은 단계는 `delayed`로 표시하며 로그인은 차단됩니다. 응답하지 않는 호출과 새 시도를 중첩하지 않고, 해당 호출이 반환된 뒤에만 다음 시도를 진행합니다. 재시도가 성공하면 자동으로 다음 단계로 진행합니다. API 로그에는 상세 오류가 기록되지만 인증 전 화면에는 단계 이름만 표시합니다. 이 준비 확인은 전체 시리얼 송수신 시험이나 이후 발생하는 WCH 커널 장애의 방지를 의미하지 않습니다. |
|
||||
|
|
||||
프론트엔드와 API를 함께 배포해야 합니다. 이전 API에 새 프론트엔드만 연결하면 명시적 준비 상태를 확인할 수 없어 로그인이 차단됩니다. |
|
||||
|
|
||||
API는 시작 시 표준 오류 로그에 `tera-api startup stage=... elapsed_ms=...`를 출력합니다. |
|
||||
|
|
||||
| 단계 | 측정 대상 | |
|
||||
| --- | --- | |
|
||||
| `config.load`, `config.persist` | 설정 읽기와 저장 | |
|
||||
| `auth.load`, `auth.bootstrap`, `auth.persist` | 계정 복원, 기본 계정 생성, 저장 | |
|
||||
| `sessions.*`, `login_failures.*`, `operations.*` | 세션, 로그인 실패, 작업 기록 복원과 저장 | |
|
||||
| `logs.retention`, `logs.journal.load` | 로그 보존 정책 적용, 기존 로그 읽기 | |
|
||||
| `store.total` | 저장소 초기화 전체 시간 (위 단계 포함) | |
|
||||
| `runtime.tls`, `runtime.snmp`, `runtime.port_services`, `runtime.management`, `runtime.serial`, `runtime.cold_start_alarm` | 각 서비스 초기 동기화 | |
|
||||
|
|
||||
`runtime synchronization completed elapsed_ms=...`는 API 프로세스 시작부터 병렬 동기화 작업 종료까지의 시간입니다. OS 부팅 전체 시간이나 브라우저 렌더링 시간은 아닙니다. 각 runtime 단계는 병렬 실행되므로 시간을 합산하지 않습니다. 단계 종료는 성공을 보장하지 않으며, 기존 `degraded` 및 `error` 로그도 함께 확인합니다. |
|
||||
|
|
||||
systemd 환경에서는 `journalctl -b -u tera-api.service`로 확인합니다. Buildroot의 `start-stop-daemon -b` 실행 경로에서는 표준 오류가 버려질 수 있으므로, 실제 장비의 서비스 실행 설정에서 표준 오류 수집을 먼저 구성해야 합니다. UI의 시스템 로그 목록에 자동 저장되는 항목은 아닙니다. |
|
||||
|
|
||||
변경 후 실제 전원 부팅에서 로그를 수집하고, 동일한 설정·로그량으로 변경 전후의 첫 UI 요청 시간을 비교합니다. `auth.bootstrap`은 저장된 계정 목록이 있는 재시작에서는 나타나지 않아야 합니다. 공장 초기 상태에서는 나타날 수 있습니다. |
|
||||
|
|
||||
이번 개선은 저장된 계정이 있을 때 불필요한 기본 계정 비밀번호 해시 생성을 생략하고, 계정·세션·로그인 실패 기록의 중복 저장을 제거합니다. 설정 마이그레이션, 로그 복원, 서비스 준비 상태 검사는 유지합니다. 실제 장비의 지연 원인과 개선 시간은 부팅 측정 후 판단합니다. |
|
||||
|
|
||||
CAPTCHA는 준비 중에도 안내 영역을 표시합니다. 실제 이미지 발급은 준비 완료 및 활성화 정책 확인 후 수행하므로 초기화 대기 중 이미지가 만료되지 않습니다. |
|
||||
|
|
||||
## 로그인 전 PC 시간 복원 |
|
||||
|
|
||||
장비 시각이 2000년 이전인 경우 로그인 준비 응답에 프로세스별 임의 토큰을 제공합니다. 로그인 페이지는 이 토큰과 현재 브라우저 UTC 시간을 `/login/bootstrap-time`으로 전송합니다. 프론트엔드는 JSON/전용 헤더와 Origin을 검사하고, API는 토큰·장비 시각·입력 범위를 검사합니다. 시간 변경은 기존 tera-timed 경계를 사용하며 설정된 시간대를 유지합니다. |
|
||||
|
|
||||
성공 후 같은 API 프로세스에서는 다시 적용하지 않습니다. 장비 시간이 이미 정상 범위라면 이 비로그인 경로로 변경할 수 없습니다. 실패 요청은 3초 간격으로 제한하며 정상 시간을 변경하려면 기존 관리자 시간 설정 기능을 사용해야 합니다. 최초 복원 시 접속 PC의 시각을 신뢰하므로 PC 시간을 정확하게 유지해야 합니다. 이 기능은 NTP 또는 RTC를 설정하거나 하드웨어 시간을 영구 저장하는 기능이 아닙니다. |
|
||||
|
|
||||
시간 복원 후에도 필수 설정 준비 확인이 끝나야 CAPTCHA 발급과 로그인이 허용됩니다. 케이블 연결만으로는 복원되지 않으며 브라우저로 로그인 페이지를 열어야 합니다. |
|
||||
|
|
||||
|
|
||||
## 전체 설정 준비 판정 (2026-09-09) |
|
||||
|
|
||||
로그인은 다음 단계가 모두 성공한 뒤 허용한다. 시간 초과나 재시도 중인 단계는 완료로 계산하지 않는다. |
|
||||
|
|
||||
- 저장소: 계정·권한·접근 제한 및 모든 running 설정 복원/검증. |
|
||||
- 시간: tera-timed가 시간대와 NTP 사용 여부/서버를 적용하고 NTP 프로세스 상태를 확인. 2000년 이전이면 로그인 화면의 기존 PC 시간 동기화 또는 NTP를 기다림. 이미 유효한 시각은 재설정하지 않음. |
|
||||
- 네트워크: tera-netd의 저장 설정과 실제 인터페이스/정적 IPv4 주소 비교. 불일치하면 running 설정 적용, 실제 주소 확인 후 트랜잭션 확정. pending 설정은 적용하지 않음. |
|
||||
- TLS·SNMP·관리 접속·포트 서비스: 각 담당 서비스의 설정 적용 성공 응답. |
|
||||
- 알림/인증 정책: 실행 중인 정책 검증, 활성 SMTP·SNMP trap·RADIUS/TACACS 자격 증명 참조 확인, 알림 데몬의 읽기 전용 health 응답. 검증을 위한 알림 전송이나 외부 로그인은 하지 않음. |
|
||||
- 로그: 저장된 로그 보존 정책 적용 응답. |
|
||||
- 시리얼: 48개 회선 설정 및 포트별 캡처/패키징/로깅 설정 적용. 마지막 서비스 검사에서 모든 채널의 available/profile_applied 확인. |
|
||||
- 초기 상태: 위 설정 적용이 끝난 뒤 새로 수집한 48개 CR 관측을 기다림. pending/confirming/error는 완료가 아님. no_response는 관측 완료로 인정하며 모든 포트에 연결 장비가 있을 것을 요구하지 않음. |
|
||||
- 마지막 서비스 검사: 시리얼과 세션 서비스 확인. |
|
||||
|
|
||||
NTP 서버 응답, 외부 SMTP 수신, 외부 인증 서버 로그인 성공은 설정 적용과 별개의 외부 가용성이다. 인터넷이 없는 환경에서도 PC 시각으로 부팅할 수 있다. 네트워크의 DHCP 임대 획득 및 외부 DDNS 갱신 성공까지 보장하는 검사는 아니다. |
|
||||
|
|
||||
배포 스크립트: `scripts/deploy-full-readiness.ps1`. API/web/timed/clone alarmd 네 파일을 SHA256 검증 후 교체하고, 120회 준비 상태 확인에서 성공하지 않으면 네 파일을 함께 복구한다. 하드웨어 커널/드라이버 코드는 변경하지 않는다. |
|
||||
@ -1,29 +0,0 @@ |
|||||
# WCH 부팅 지연 진단 — 2026-09-09 |
|
||||
|
|
||||
## 장비에서 확인한 사실 |
|
||||
|
|
||||
- 커널: `5.10.160 #22`, ARM64, TERA CORA-X48 PRO K FET3568J-C. |
|
||||
- 부팅 150.52초와 279.04초에 CPU0 RCU stall 발생. |
|
||||
- 279초 덤프의 CPU0 PC가 `READ_UART_IIR.isra.0`, `wch_ser_interrupt`에 반복적으로 위치함. |
|
||||
- 319.47초 `irq 95: nobody cared`, 핸들러 `wch_interrupt`, 이어서 `Disabling IRQ #95`. |
|
||||
- PCI `0000:01:00.0`, vendor/device `1c00:4353`, IRQ95, 드라이버 `wchpciserial`. |
|
||||
- 장비 매핑에서 이 PCI 장치는 CH01–CH24에 해당함. 다른 장치 `0002:21:00.0`은 IRQ99. |
|
||||
- `modules.builtin`에 `drivers/tty/serial/wch/wch.ko`, `wch_pre.ko`가 등록되어 있음. 현재 드라이버는 커널 내장 구성. |
|
||||
- CR 자동 탐색: 부팅 30초 뒤 시작, 5초 주기, 응답 제한 150ms, 채널 간 20ms. 모뎀 링크 스캔은 꺼져 있음. |
|
||||
- 측정 시 가용 메모리 약 1.5GB. 웹이 실제 사용하는 API는 `127.0.0.1:8084`; 장애 후 로그인 정책 요청은 약 14ms. |
|
||||
|
|
||||
## 판단과 한계 |
|
||||
|
|
||||
WCH 인터럽트 처리 경로의 커널 정체가 확인됐다. UI 지연과 관련될 가능성이 높지만, 자동 탐색·포트 초기 설정·외부 입력 중 어떤 동작이 최초 촉발 조건인지는 아직 분리 검증하지 않았다. 150초 스택 자체는 파일 조회/블록 I/O 경로이므로 모든 stall을 동일한 원인으로 단정하지 않는다. |
|
||||
|
|
||||
IRQ 비활성화 후 UI 응답이 빨라져도 시리얼 통신 복구를 의미하지 않는다. 초기 계정 해시 생성을 줄인 API 변경은 이 커널 장애를 해결하지 않는다. |
|
||||
|
|
||||
## 다음 수정에 필요한 것 |
|
||||
|
|
||||
실제 커널 #22의 BSP 소스, 설정, 빌드 도구 및 WCH 드라이버 패치가 필요하다. 현재 작업 공간과 검색한 사용자 폴더에서는 해당 소스를 찾지 못했고 장비에도 `/usr/src`가 없다. 매핑 파일은 `target-bsp/docs/snapshots/20260721-wch-legacy-intx-loopback/manifest.json`을 근거로 참조한다. |
|
||||
|
|
||||
소스 확보 후 `wch_ser_interrupt`의 반복 종료 조건, IIR 읽기 및 인터럽트 원인 제거, IRQ 반환값과 PCI INTx 처리, 포트 open/close/termios 적용 시 인터럽트 상태 변경을 확인한다. 코드 검토 전 `irqpoll` 추가, IRQ 강제 재활성화, 드라이버 교체는 적용하지 않았다. |
|
||||
|
|
||||
수정 검증은 부팅 초기부터 커널 로그를 연속 수집하며 UI 응답과 CH01–CH48 실제 송수신을 함께 확인해야 한다. 자동 탐색을 일시 중지하는 비교 실험은 기능 변화와 복원 절차를 명시하고 수행한다. |
|
||||
|
|
||||
원시 로그: `../runtime/perf-console-20260909.log`. |
|
||||
@ -1,26 +0,0 @@ |
|||||
name: tera-observability-demo |
|
||||
|
|
||||
services: |
|
||||
mailpit: |
|
||||
image: axllent/mailpit:v1.27.4 |
|
||||
restart: "no" |
|
||||
ports: |
|
||||
- "${DEMO_BIND_ADDRESS:-127.0.0.1}:1025:1025/tcp" |
|
||||
- "127.0.0.1:8025:8025/tcp" |
|
||||
environment: |
|
||||
MP_DATABASE: /data/mailpit.db |
|
||||
volumes: |
|
||||
- mail-data:/data |
|
||||
|
|
||||
syslog: |
|
||||
build: ./syslog |
|
||||
restart: "no" |
|
||||
ports: |
|
||||
- "${DEMO_BIND_ADDRESS:-127.0.0.1}:1514:514/udp" |
|
||||
- "${DEMO_BIND_ADDRESS:-127.0.0.1}:1514:514/tcp" |
|
||||
volumes: |
|
||||
- syslog-data:/var/log/tera |
|
||||
|
|
||||
volumes: |
|
||||
mail-data: |
|
||||
syslog-data: |
|
||||
@ -1,10 +0,0 @@ |
|||||
FROM ubuntu:24.04 |
|
||||
RUN apt-get update \ |
|
||||
&& apt-get install -y --no-install-recommends rsyslog \ |
|
||||
&& rm -rf /var/lib/apt/lists/* \ |
|
||||
&& mkdir -p /var/log/tera \ |
|
||||
&& touch /var/log/tera/remote.log |
|
||||
COPY rsyslog.conf /etc/rsyslog.conf |
|
||||
RUN rsyslogd -N1 |
|
||||
EXPOSE 514/udp 514/tcp |
|
||||
CMD ["rsyslogd", "-n", "-i", "/tmp/rsyslog.pid"] |
|
||||
@ -1,7 +0,0 @@ |
|||||
module(load="imudp") |
|
||||
module(load="imtcp") |
|
||||
input(type="imudp" port="514") |
|
||||
input(type="imtcp" port="514") |
|
||||
template(name="TeraDemo" type="string" |
|
||||
string="%timegenerated:::date-rfc3339% from=%fromhost-ip% %rawmsg%\n") |
|
||||
action(type="omfile" file="/var/log/tera/remote.log" template="TeraDemo") |
|
||||
@ -1,120 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/bootstrap-time-transfer.log') -Force | Out-Null |
|
||||
$binary = Join-Path $project 'build/linux-arm64/tera-clone-demo-api-bootstrap-time' |
|
||||
$webBinary = Join-Path $project 'build/linux-arm64/tera-clone-demo-web-bootstrap-time' |
|
||||
$webHash = (Get-FileHash -LiteralPath $webBinary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$script = @' |
|
||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-bootstrap-time-20260909 |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -T 20 -O "$stage/api" http://192.168.0.100:9001/api |
|
||||
wget -q -T 20 -O "$stage/web" http://192.168.0.100:9001/web |
|
||||
echo '__HASH__ '"$stage/api" | sha256sum -c - |
|
||||
echo '__WEBHASH__ '"$stage/web" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/bootstrap-time-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-clone-demo-api "$backup/api" |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/web" |
|
||||
echo "BACKUP=$backup" |
|
||||
cp "$stage/api" /usr/bin/tera-clone-demo-api.next |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-web.next |
|
||||
rollback() { |
|
||||
trap - EXIT |
|
||||
/etc/init.d/S61tera-clone-demo-web stop || true |
|
||||
/etc/init.d/S51tera-clone-demo-api stop || true |
|
||||
sleep 2 |
|
||||
cp "$backup/api" /usr/bin/tera-clone-demo-api.restore |
|
||||
cp "$backup/web" /usr/bin/tera-clone-demo-web.restore |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-web.restore |
|
||||
mv /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-api |
|
||||
mv /usr/bin/tera-clone-demo-web.restore /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S51tera-clone-demo-api start |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
echo BOOTSTRAP_TIME_ROLLED_BACK |
|
||||
} |
|
||||
trap rollback EXIT |
|
||||
/etc/init.d/S61tera-clone-demo-web stop |
|
||||
/etc/init.d/S51tera-clone-demo-api stop |
|
||||
sleep 2 |
|
||||
mv /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-api |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
( |
|
||||
set -a |
|
||||
. /etc/tera/tera-clone-demo-api.env |
|
||||
set +a |
|
||||
start-stop-daemon -S -q -b -m -p /run/tera-clone-demo-api.pid -c tera:tera -x /bin/sh -- -c 'exec /usr/bin/tera-clone-demo-api > /run/tera-clone-demo-api/bootstrap-time-startup.log 2>&1' |
|
||||
) |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
ready=0 |
|
||||
for n in $(seq 1 45); do |
|
||||
if wget -q -T 2 -O "$stage/status" http://127.0.0.1:8083/login/backend-status; then |
|
||||
if grep -q '"ready":true' "$stage/status"; then ready=1; break; fi |
|
||||
if grep -q '"state":"failed"' "$stage/status"; then cat "$stage/status"; grep -E "startup|reconciliation" /run/tera-clone-demo-api/bootstrap-time-startup.log; exit 1; fi |
|
||||
fi |
|
||||
sleep 1 |
|
||||
done |
|
||||
cat "$stage/status" |
|
||||
[ "$ready" = 1 ] |
|
||||
wget -q -T 3 -O "$stage/login.js" http://127.0.0.1:8083/static/js/login.js |
|
||||
grep -q '/login/bootstrap-time' "$stage/login.js" |
|
||||
sha256sum /usr/bin/tera-clone-demo-api /usr/bin/tera-clone-demo-web |
|
||||
sync |
|
||||
trap - EXIT |
|
||||
echo BOOTSTRAP_TIME_DEPLOY_OK |
|
||||
'@ |
|
||||
$script = $script.Replace('__HASH__', $hash).Replace('__WEBHASH__', $webHash).Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-bootstrap-time-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM5',115200,'None',8,'One' |
|
||||
$serial.Encoding = [Text.Encoding]::UTF8 |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-bootstrap-time.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-bootstrap-time.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(180) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
switch ($context.Request.Url.AbsolutePath) { |
|
||||
'/deploy.sh' { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
'/web' { $bytes = [IO.File]::ReadAllBytes($webBinary) } |
|
||||
'/api' { $bytes = [IO.File]::ReadAllBytes($binary) } |
|
||||
default { $context.Response.StatusCode = 404 } |
|
||||
} |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('BOOTSTRAP_TIME_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } |
|
||||
if ($output.Contains('BOOTSTRAP_TIME_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('BOOTSTRAP_TIME_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
if (-not $output.Contains($hash) -or -not $output.Contains($webHash)) { throw 'Deployed hash missing from console output.' } |
|
||||
Write-Output "VERIFIED deployed SHA256=$hash and API login-policy endpoint" |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,87 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/close-all-tabs-transfer.log') -Force | Out-Null |
|
||||
$binary = Join-Path $project 'build/linux-arm64/tera-clone-demo-web' |
|
||||
$hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$script = @' |
|
||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-close-all-tabs-20260908 |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -O "$stage/web" http://192.168.0.100:9001/web |
|
||||
echo '__HASH__ '"$stage/web" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/close-all-tabs-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/tera-clone-demo-web" |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-web.next |
|
||||
/etc/init.d/S61tera-clone-demo-web stop |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
if ! /etc/init.d/S61tera-clone-demo-web start; then |
|
||||
cp "$backup/tera-clone-demo-web" /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
exit 1 |
|
||||
fi |
|
||||
sleep 2 |
|
||||
if ! wget -q -O "$stage/status" http://127.0.0.1:8083/login/backend-status; then |
|
||||
/etc/init.d/S61tera-clone-demo-web stop |
|
||||
cp "$backup/tera-clone-demo-web" /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
exit 1 |
|
||||
fi |
|
||||
cat "$stage/status" |
|
||||
sha256sum /usr/bin/tera-clone-demo-web |
|
||||
echo "BACKUP=$backup" |
|
||||
echo CLOSE_ALL_TABS_DEPLOY_OK |
|
||||
'@ |
|
||||
$script = $script.Replace('__HASH__', $hash).Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-close-all-tabs-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM5',115200,'None',8,'One' |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-close-all-tabs.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-close-all-tabs.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(90) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
switch ($context.Request.Url.AbsolutePath) { |
|
||||
'/deploy.sh' { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
'/web' { $bytes = [IO.File]::ReadAllBytes($binary) } |
|
||||
default { $context.Response.StatusCode = 404 } |
|
||||
} |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('CLOSE_ALL_TABS_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('CLOSE_ALL_TABS_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
if (-not $output.Contains($hash)) { throw 'Deployed hash missing from console output.' } |
|
||||
$response = Invoke-WebRequest -UseBasicParsing 'http://192.168.0.223:8083/static/js/app.js?v=20260908-close-all-tabs-1' |
|
||||
if (-not $response.Content.Contains('function closeAllWorkspaceTabs()') -or -not $response.Content.Contains('event.altKey')) { throw 'Device is not serving the updated script.' } |
|
||||
Write-Output "VERIFIED deployed SHA256=$hash and Alt-click JavaScript" |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,56 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/config-read-transfer.log') -Force | Out-Null |
|
||||
$assets = @{'/api'=(Join-Path $project 'build/linux-arm64/tera-clone-demo-api'); '/web'=(Join-Path $project 'build/linux-arm64/tera-clone-demo-web')} |
|
||||
$hashes = @{} |
|
||||
foreach ($key in $assets.Keys) {$hashes[$key]=(Get-FileHash $assets[$key] -Algorithm SHA256).Hash.ToLowerInvariant()} |
|
||||
$script=[IO.File]::ReadAllText((Join-Path $PSScriptRoot 'deploy-config-read.sh')).Replace('__APIHASH__',$hashes['/api']).Replace('__WEBHASH__',$hashes['/web']).Replace("`r`n","`n") |
|
||||
$rule = 'TERA-replacement-default-port-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One' |
|
||||
$serial.Encoding = [Text.Encoding]::UTF8 |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-replacement-default-port.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-replacement-default-port.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(360) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
$route = $context.Request.Url.AbsolutePath |
|
||||
if ($route -eq '/deploy.sh') { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
elseif ($assets.ContainsKey($route)) { $bytes = [IO.File]::ReadAllBytes($assets[$route]) } |
|
||||
else { $context.Response.StatusCode = 404 } |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('REPLACEMENT_DEFAULT_PORT_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } |
|
||||
if ($output.Contains('REPLACEMENT_DEFAULT_PORT_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('REPLACEMENT_DEFAULT_PORT_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
foreach ($hash in $hashes.Values) { if (-not $output.Contains($hash)) { throw 'A deployed hash was not verified.' } } |
|
||||
Write-Output 'VERIFIED replacement device deployment on HTTP port 80' |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,53 +0,0 @@ |
|||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-config-read |
|
||||
mkdir -p "$stage" |
|
||||
for name in api web; do wget -q -T 30 -O "$stage/$name" "http://192.168.0.100:9001/$name"; done |
|
||||
echo '__APIHASH__ '"$stage/api" | sha256sum -c - |
|
||||
echo '__WEBHASH__ '"$stage/web" | sha256sum -c - |
|
||||
grep -q '^PORT=80$' /etc/tera/teraweb.env |
|
||||
backup=/userdata/tera-backups/config-read-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-api "$backup/api" |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/web" |
|
||||
echo "BACKUP=$backup" |
|
||||
rollback() { |
|
||||
trap - EXIT |
|
||||
/etc/init.d/S61teraweb stop || true |
|
||||
/etc/init.d/S50tera-api stop || true |
|
||||
cp "$backup/api" /usr/bin/tera-api.restore |
|
||||
cp "$backup/web" /usr/bin/tera-clone-demo-web.restore |
|
||||
chmod 755 /usr/bin/tera-api.restore /usr/bin/tera-clone-demo-web.restore |
|
||||
mv /usr/bin/tera-api.restore /usr/bin/tera-api |
|
||||
mv /usr/bin/tera-clone-demo-web.restore /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S50tera-api start || true |
|
||||
/etc/init.d/S61teraweb start || true |
|
||||
echo REPLACEMENT_DEFAULT_PORT_ROLLED_BACK |
|
||||
} |
|
||||
trap rollback EXIT |
|
||||
cp "$stage/api" /usr/bin/tera-api.next |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 755 /usr/bin/tera-api.next /usr/bin/tera-clone-demo-web.next |
|
||||
/etc/init.d/S61teraweb stop |
|
||||
/etc/init.d/S50tera-api stop |
|
||||
mv /usr/bin/tera-api.next /usr/bin/tera-api |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
set -a |
|
||||
. /etc/tera/tera-api.env |
|
||||
set +a |
|
||||
start-stop-daemon -S -q -b -m -p /run/tera-api.pid -c tera:tera -x /bin/sh -- -c 'exec /usr/bin/tera-api > /run/tera-api/config-read-startup.log 2>&1' |
|
||||
/etc/init.d/S61teraweb start |
|
||||
ready=0 |
|
||||
for n in $(seq 1 120); do |
|
||||
if wget -q -T 2 -O "$stage/status" http://127.0.0.1/login/backend-status && grep -q '"ready":true' "$stage/status"; then ready=1; break; fi |
|
||||
sleep 1 |
|
||||
done |
|
||||
cat "$stage/status" |
|
||||
[ "$ready" = 1 ] |
|
||||
wget -q -T 3 -O "$stage/integration.js" http://127.0.0.1/static/js/integration.js |
|
||||
grep -q 'limit=48&runtime=false' "$stage/integration.js" |
|
||||
grep -q 'pendingNavigation' "$stage/integration.js" |
|
||||
sha256sum /usr/bin/tera-api /usr/bin/tera-clone-demo-web |
|
||||
sync |
|
||||
trap - EXIT |
|
||||
echo REPLACEMENT_DEFAULT_PORT_DEPLOY_OK |
|
||||
@ -1,87 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/firmware-progress-transfer.log') -Force | Out-Null |
|
||||
$binary = Join-Path $project 'build/linux-arm64/tera-clone-demo-web-firmware-progress' |
|
||||
$hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$script = @' |
|
||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-firmware-progress-20260908 |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -O "$stage/web" http://192.168.0.100:9001/web |
|
||||
echo '__HASH__ '"$stage/web" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/firmware-progress-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/tera-clone-demo-web" |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-web.next |
|
||||
/etc/init.d/S61tera-clone-demo-web stop |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
if ! /etc/init.d/S61tera-clone-demo-web start; then |
|
||||
cp "$backup/tera-clone-demo-web" /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
exit 1 |
|
||||
fi |
|
||||
sleep 2 |
|
||||
if ! wget -q -O "$stage/status" http://127.0.0.1:8083/login/backend-status; then |
|
||||
/etc/init.d/S61tera-clone-demo-web stop |
|
||||
cp "$backup/tera-clone-demo-web" /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
exit 1 |
|
||||
fi |
|
||||
cat "$stage/status" |
|
||||
sha256sum /usr/bin/tera-clone-demo-web |
|
||||
echo "BACKUP=$backup" |
|
||||
echo FIRMWARE_PROGRESS_DEPLOY_OK |
|
||||
'@ |
|
||||
$script = $script.Replace('__HASH__', $hash).Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-firmware-progress-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM5',115200,'None',8,'One' |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-firmware-progress.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-firmware-progress.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(90) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
switch ($context.Request.Url.AbsolutePath) { |
|
||||
'/deploy.sh' { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
'/web' { $bytes = [IO.File]::ReadAllBytes($binary) } |
|
||||
default { $context.Response.StatusCode = 404 } |
|
||||
} |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('FIRMWARE_PROGRESS_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('FIRMWARE_PROGRESS_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
if (-not $output.Contains($hash)) { throw 'Deployed hash missing from console output.' } |
|
||||
$response = Invoke-WebRequest -UseBasicParsing 'http://192.168.0.223:8083/static/js/firmware-progress.js?v=20260908-firmware-progress-1' |
|
||||
if (-not $response.Content.Contains('펌웨어 업데이트 완료') -or -not $response.Content.Contains('/api/v1/firmware/status')) { throw 'Device is not serving the updated script.' } |
|
||||
Write-Output "VERIFIED deployed SHA256=$hash and firmware progress JavaScript" |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,153 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/full-readiness-transfer.log') -Force | Out-Null |
|
||||
$binary = Join-Path $project 'build/linux-arm64/tera-clone-demo-api-full-readiness' |
|
||||
$webBinary = Join-Path $project 'build/linux-arm64/tera-clone-demo-web-full-readiness' |
|
||||
$timeBinary = Join-Path $project 'build/linux-arm64/tera-timed-full-readiness' |
|
||||
$alarmBinary = Join-Path $project 'build/linux-arm64/tera-clone-demo-alarmd-full-readiness' |
|
||||
$timeHash = (Get-FileHash -LiteralPath $timeBinary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$alarmHash = (Get-FileHash -LiteralPath $alarmBinary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$webHash = (Get-FileHash -LiteralPath $webBinary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$script = @' |
|
||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-full-readiness-20260909 |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -T 20 -O "$stage/api" http://192.168.0.100:9001/api |
|
||||
wget -q -T 20 -O "$stage/web" http://192.168.0.100:9001/web |
|
||||
wget -q -T 20 -O "$stage/timed" http://192.168.0.100:9001/timed |
|
||||
wget -q -T 20 -O "$stage/alarmd" http://192.168.0.100:9001/alarmd |
|
||||
echo '__TIMEHASH__ '"$stage/timed" | sha256sum -c - |
|
||||
echo '__ALARMHASH__ '"$stage/alarmd" | sha256sum -c - |
|
||||
echo '__HASH__ '"$stage/api" | sha256sum -c - |
|
||||
echo '__WEBHASH__ '"$stage/web" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/full-readiness-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-clone-demo-api "$backup/api" |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/web" |
|
||||
cp -p /usr/bin/tera-timed "$backup/timed" |
|
||||
cp -p /usr/bin/tera-clone-demo-alarmd "$backup/alarmd" |
|
||||
echo "BACKUP=$backup" |
|
||||
cp "$stage/api" /usr/bin/tera-clone-demo-api.next |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-web.next |
|
||||
cp "$stage/timed" /usr/bin/tera-timed.next |
|
||||
cp "$stage/alarmd" /usr/bin/tera-clone-demo-alarmd.next |
|
||||
chmod 0755 /usr/bin/tera-timed.next /usr/bin/tera-clone-demo-alarmd.next |
|
||||
rollback() { |
|
||||
trap - EXIT |
|
||||
/etc/init.d/S61tera-clone-demo-web stop || true |
|
||||
/etc/init.d/S51tera-clone-demo-api stop || true |
|
||||
/etc/init.d/S48tera-clone-demo-alarmd stop || true |
|
||||
/etc/init.d/S45tera-timed stop || true |
|
||||
sleep 2 |
|
||||
cp "$backup/api" /usr/bin/tera-clone-demo-api.restore |
|
||||
cp "$backup/web" /usr/bin/tera-clone-demo-web.restore |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-web.restore |
|
||||
mv /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-api |
|
||||
mv /usr/bin/tera-clone-demo-web.restore /usr/bin/tera-clone-demo-web |
|
||||
cp "$backup/timed" /usr/bin/tera-timed.restore |
|
||||
cp "$backup/alarmd" /usr/bin/tera-clone-demo-alarmd.restore |
|
||||
chmod 0755 /usr/bin/tera-timed.restore /usr/bin/tera-clone-demo-alarmd.restore |
|
||||
mv /usr/bin/tera-timed.restore /usr/bin/tera-timed |
|
||||
mv /usr/bin/tera-clone-demo-alarmd.restore /usr/bin/tera-clone-demo-alarmd |
|
||||
/etc/init.d/S45tera-timed start |
|
||||
/etc/init.d/S48tera-clone-demo-alarmd start |
|
||||
/etc/init.d/S51tera-clone-demo-api start |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
echo FULL_READINESS_ROLLED_BACK |
|
||||
} |
|
||||
trap rollback EXIT |
|
||||
/etc/init.d/S61tera-clone-demo-web stop |
|
||||
/etc/init.d/S51tera-clone-demo-api stop |
|
||||
/etc/init.d/S48tera-clone-demo-alarmd stop |
|
||||
/etc/init.d/S45tera-timed stop |
|
||||
sleep 2 |
|
||||
mv /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-api |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
mv /usr/bin/tera-timed.next /usr/bin/tera-timed |
|
||||
mv /usr/bin/tera-clone-demo-alarmd.next /usr/bin/tera-clone-demo-alarmd |
|
||||
/etc/init.d/S45tera-timed start |
|
||||
/etc/init.d/S48tera-clone-demo-alarmd start |
|
||||
( |
|
||||
set -a |
|
||||
. /etc/tera/tera-clone-demo-api.env |
|
||||
set +a |
|
||||
start-stop-daemon -S -q -b -m -p /run/tera-clone-demo-api.pid -c tera:tera -x /bin/sh -- -c 'exec /usr/bin/tera-clone-demo-api > /run/tera-clone-demo-api/full-readiness-startup.log 2>&1' |
|
||||
) |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
ready=0 |
|
||||
for n in $(seq 1 120); do |
|
||||
if wget -q -T 2 -O "$stage/status" http://127.0.0.1:8083/login/backend-status; then |
|
||||
if grep -q '"ready":true' "$stage/status"; then ready=1; break; fi |
|
||||
if grep -q '"state":"failed"' "$stage/status"; then cat "$stage/status"; grep -E "startup|reconciliation" /run/tera-clone-demo-api/full-readiness-startup.log; exit 1; fi |
|
||||
fi |
|
||||
sleep 1 |
|
||||
done |
|
||||
cat "$stage/status" |
|
||||
if [ "$ready" != 1 ]; then |
|
||||
grep -E "startup|reconciliation|observation" /run/tera-clone-demo-api/full-readiness-startup.log || true |
|
||||
exit 1 |
|
||||
fi |
|
||||
wget -q -T 3 -O "$stage/login.js" http://127.0.0.1:8083/static/js/login.js |
|
||||
grep -q '/login/bootstrap-time' "$stage/login.js" |
|
||||
sha256sum /usr/bin/tera-clone-demo-api /usr/bin/tera-clone-demo-web /usr/bin/tera-timed /usr/bin/tera-clone-demo-alarmd |
|
||||
sync |
|
||||
trap - EXIT |
|
||||
echo FULL_READINESS_DEPLOY_OK |
|
||||
'@ |
|
||||
$script = $script.Replace('__HASH__', $hash).Replace('__WEBHASH__', $webHash).Replace('__TIMEHASH__', $timeHash).Replace('__ALARMHASH__', $alarmHash).Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-full-readiness-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM5',115200,'None',8,'One' |
|
||||
$serial.Encoding = [Text.Encoding]::UTF8 |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-full-readiness.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-full-readiness.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(360) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
switch ($context.Request.Url.AbsolutePath) { |
|
||||
'/deploy.sh' { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
'/web' { $bytes = [IO.File]::ReadAllBytes($webBinary) } |
|
||||
'/api' { $bytes = [IO.File]::ReadAllBytes($binary) } |
|
||||
'/timed' { $bytes = [IO.File]::ReadAllBytes($timeBinary) } |
|
||||
'/alarmd' { $bytes = [IO.File]::ReadAllBytes($alarmBinary) } |
|
||||
default { $context.Response.StatusCode = 404 } |
|
||||
} |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('FULL_READINESS_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } |
|
||||
if ($output.Contains('FULL_READINESS_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('FULL_READINESS_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
if (-not $output.Contains($hash) -or -not $output.Contains($webHash) -or -not $output.Contains($timeHash) -or -not $output.Contains($alarmHash)) { throw 'Deployed hash missing from console output.' } |
|
||||
Write-Output "VERIFIED deployed SHA256=$hash and API login-policy endpoint" |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,119 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/login-ready-transfer.log') -Force | Out-Null |
|
||||
$binary = Join-Path $project 'build/linux-arm64/tera-clone-demo-api-login-ready' |
|
||||
$webBinary = Join-Path $project 'build/linux-arm64/tera-clone-demo-web-login-ready' |
|
||||
$webHash = (Get-FileHash -LiteralPath $webBinary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$script = @' |
|
||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-login-ready-20260909 |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -T 20 -O "$stage/api" http://192.168.0.100:9001/api |
|
||||
wget -q -T 20 -O "$stage/web" http://192.168.0.100:9001/web |
|
||||
echo '__HASH__ '"$stage/api" | sha256sum -c - |
|
||||
echo '__WEBHASH__ '"$stage/web" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/login-ready-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-clone-demo-api "$backup/api" |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/web" |
|
||||
echo "BACKUP=$backup" |
|
||||
cp "$stage/api" /usr/bin/tera-clone-demo-api.next |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-web.next |
|
||||
rollback() { |
|
||||
trap - EXIT |
|
||||
/etc/init.d/S61tera-clone-demo-web stop || true |
|
||||
/etc/init.d/S51tera-clone-demo-api stop || true |
|
||||
sleep 2 |
|
||||
cp "$backup/api" /usr/bin/tera-clone-demo-api.restore |
|
||||
cp "$backup/web" /usr/bin/tera-clone-demo-web.restore |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-web.restore |
|
||||
mv /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-api |
|
||||
mv /usr/bin/tera-clone-demo-web.restore /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S51tera-clone-demo-api start |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
echo LOGIN_READY_ROLLED_BACK |
|
||||
} |
|
||||
trap rollback EXIT |
|
||||
/etc/init.d/S61tera-clone-demo-web stop |
|
||||
/etc/init.d/S51tera-clone-demo-api stop |
|
||||
sleep 2 |
|
||||
mv /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-api |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
( |
|
||||
set -a |
|
||||
. /etc/tera/tera-clone-demo-api.env |
|
||||
set +a |
|
||||
start-stop-daemon -S -q -b -m -p /run/tera-clone-demo-api.pid -c tera:tera -x /bin/sh -- -c 'exec /usr/bin/tera-clone-demo-api > /run/tera-clone-demo-api/login-ready-startup.log 2>&1' |
|
||||
) |
|
||||
/etc/init.d/S61tera-clone-demo-web start |
|
||||
ready=0 |
|
||||
for n in $(seq 1 45); do |
|
||||
if wget -q -T 2 -O "$stage/status" http://127.0.0.1:8083/login/backend-status; then |
|
||||
if grep -q '"ready":true' "$stage/status"; then ready=1; break; fi |
|
||||
if grep -q '"state":"failed"' "$stage/status"; then cat "$stage/status"; grep -E "startup|reconciliation" /run/tera-clone-demo-api/login-ready-startup.log; exit 1; fi |
|
||||
fi |
|
||||
sleep 1 |
|
||||
done |
|
||||
cat "$stage/status" |
|
||||
[ "$ready" = 1 ] |
|
||||
wget -q -T 3 -O "$stage/login.js" http://127.0.0.1:8083/static/js/login.js |
|
||||
grep -q 'function showStartup' "$stage/login.js" |
|
||||
sha256sum /usr/bin/tera-clone-demo-api /usr/bin/tera-clone-demo-web |
|
||||
sync |
|
||||
trap - EXIT |
|
||||
echo LOGIN_READY_DEPLOY_OK |
|
||||
'@ |
|
||||
$script = $script.Replace('__HASH__', $hash).Replace('__WEBHASH__', $webHash).Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-login-ready-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM5',115200,'None',8,'One' |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-login-ready.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-login-ready.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(180) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
switch ($context.Request.Url.AbsolutePath) { |
|
||||
'/deploy.sh' { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
'/web' { $bytes = [IO.File]::ReadAllBytes($webBinary) } |
|
||||
'/api' { $bytes = [IO.File]::ReadAllBytes($binary) } |
|
||||
default { $context.Response.StatusCode = 404 } |
|
||||
} |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('LOGIN_READY_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } |
|
||||
if ($output.Contains('LOGIN_READY_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('LOGIN_READY_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
if (-not $output.Contains($hash) -or -not $output.Contains($webHash)) { throw 'Deployed hash missing from console output.' } |
|
||||
Write-Output "VERIFIED deployed SHA256=$hash and API login-policy endpoint" |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,215 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/replacement-default-port-transfer.log') -Force | Out-Null |
|
||||
$assets = @{} |
|
||||
$hashes = @{} |
|
||||
$assets['/api'] = Join-Path $project 'final-build/backend/tera-clone-demo-api' |
|
||||
$assets['/web'] = Join-Path $project 'final-build/frontend/tera-clone-demo-web' |
|
||||
$assets['/timed'] = Join-Path $project 'final-build/backend/tera-timed' |
|
||||
$assets['/alarmd'] = Join-Path $project 'final-build/backend/tera-clone-demo-alarmd' |
|
||||
$assets['/seriald'] = Join-Path $project 'final-build/backend/tera-seriald' |
|
||||
$assets['/logd'] = Join-Path $project 'final-build/backend/tera-logd' |
|
||||
$assets['/netd'] = Join-Path $project 'final-build/backend/tera-netd' |
|
||||
$assets['/sessiond'] = Join-Path $project 'final-build/backend/tera-sessiond' |
|
||||
foreach ($key in $assets.Keys) { $hashes[$key] = (Get-FileHash -LiteralPath $assets[$key] -Algorithm SHA256).Hash.ToLowerInvariant() } |
|
||||
$script = @' |
|
||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-replacement-default-port |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -T 25 -O "$stage/api" http://192.168.0.100:9001/api |
|
||||
echo '__APIHASH__ '"$stage/api" | sha256sum -c - |
|
||||
wget -q -T 25 -O "$stage/web" http://192.168.0.100:9001/web |
|
||||
echo '__WEBHASH__ '"$stage/web" | sha256sum -c - |
|
||||
wget -q -T 25 -O "$stage/timed" http://192.168.0.100:9001/timed |
|
||||
echo '__TIMEDHASH__ '"$stage/timed" | sha256sum -c - |
|
||||
wget -q -T 25 -O "$stage/alarmd" http://192.168.0.100:9001/alarmd |
|
||||
echo '__ALARMDHASH__ '"$stage/alarmd" | sha256sum -c - |
|
||||
wget -q -T 25 -O "$stage/seriald" http://192.168.0.100:9001/seriald |
|
||||
echo '__SERIALDHASH__ '"$stage/seriald" | sha256sum -c - |
|
||||
wget -q -T 25 -O "$stage/logd" http://192.168.0.100:9001/logd |
|
||||
echo '__LOGDHASH__ '"$stage/logd" | sha256sum -c - |
|
||||
wget -q -T 25 -O "$stage/netd" http://192.168.0.100:9001/netd |
|
||||
echo '__NETDHASH__ '"$stage/netd" | sha256sum -c - |
|
||||
wget -q -T 25 -O "$stage/sessiond" http://192.168.0.100:9001/sessiond |
|
||||
echo '__SESSIONDHASH__ '"$stage/sessiond" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/replacement-default-port-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /etc/tera/teraweb.env "$backup/teraweb.env" |
|
||||
echo "BACKUP=$backup" |
|
||||
cp -p /usr/bin/tera-api "$backup/api" |
|
||||
cp "$stage/api" /usr/bin/tera-api.next |
|
||||
chmod 0755 /usr/bin/tera-api.next |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/web" |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-web.next |
|
||||
cp -p /usr/bin/tera-timed "$backup/timed" |
|
||||
cp "$stage/timed" /usr/bin/tera-timed.next |
|
||||
chmod 0755 /usr/bin/tera-timed.next |
|
||||
cp -p /usr/bin/tera-alarmd "$backup/alarmd" |
|
||||
cp "$stage/alarmd" /usr/bin/tera-alarmd.next |
|
||||
chmod 0755 /usr/bin/tera-alarmd.next |
|
||||
cp -p /usr/bin/tera-seriald "$backup/seriald" |
|
||||
cp "$stage/seriald" /usr/bin/tera-seriald.next |
|
||||
chmod 0755 /usr/bin/tera-seriald.next |
|
||||
cp -p /usr/bin/tera-logd "$backup/logd" |
|
||||
cp "$stage/logd" /usr/bin/tera-logd.next |
|
||||
chmod 0755 /usr/bin/tera-logd.next |
|
||||
cp -p /usr/bin/tera-netd "$backup/netd" |
|
||||
cp "$stage/netd" /usr/bin/tera-netd.next |
|
||||
chmod 0755 /usr/bin/tera-netd.next |
|
||||
cp -p /usr/bin/tera-sessiond "$backup/sessiond" |
|
||||
cp "$stage/sessiond" /usr/bin/tera-sessiond.next |
|
||||
chmod 0755 /usr/bin/tera-sessiond.next |
|
||||
rollback() { |
|
||||
trap - EXIT |
|
||||
/etc/init.d/S61teraweb stop || true |
|
||||
/etc/init.d/S55tera-sessiond stop || true |
|
||||
/etc/init.d/S50tera-api stop || true |
|
||||
/etc/init.d/S40tera-seriald stop || true |
|
||||
/etc/init.d/S47tera-alarmd stop || true |
|
||||
/etc/init.d/S43tera-netd stop || true |
|
||||
/etc/init.d/S45tera-timed stop || true |
|
||||
/etc/init.d/S44tera-logd stop || true |
|
||||
sleep 2 |
|
||||
cp "$backup/api" /usr/bin/tera-api.restore |
|
||||
chmod 0755 /usr/bin/tera-api.restore |
|
||||
mv /usr/bin/tera-api.restore /usr/bin/tera-api |
|
||||
cp "$backup/web" /usr/bin/tera-clone-demo-web.restore |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-web.restore |
|
||||
mv /usr/bin/tera-clone-demo-web.restore /usr/bin/tera-clone-demo-web |
|
||||
cp "$backup/timed" /usr/bin/tera-timed.restore |
|
||||
chmod 0755 /usr/bin/tera-timed.restore |
|
||||
mv /usr/bin/tera-timed.restore /usr/bin/tera-timed |
|
||||
cp "$backup/alarmd" /usr/bin/tera-alarmd.restore |
|
||||
chmod 0755 /usr/bin/tera-alarmd.restore |
|
||||
mv /usr/bin/tera-alarmd.restore /usr/bin/tera-alarmd |
|
||||
cp "$backup/seriald" /usr/bin/tera-seriald.restore |
|
||||
chmod 0755 /usr/bin/tera-seriald.restore |
|
||||
mv /usr/bin/tera-seriald.restore /usr/bin/tera-seriald |
|
||||
cp "$backup/logd" /usr/bin/tera-logd.restore |
|
||||
chmod 0755 /usr/bin/tera-logd.restore |
|
||||
mv /usr/bin/tera-logd.restore /usr/bin/tera-logd |
|
||||
cp "$backup/netd" /usr/bin/tera-netd.restore |
|
||||
chmod 0755 /usr/bin/tera-netd.restore |
|
||||
mv /usr/bin/tera-netd.restore /usr/bin/tera-netd |
|
||||
/etc/init.d/S43tera-netd start || true |
|
||||
/etc/init.d/S44tera-logd start || true |
|
||||
/etc/init.d/S40tera-seriald start || true |
|
||||
/etc/init.d/S45tera-timed start || true |
|
||||
/etc/init.d/S47tera-alarmd start || true |
|
||||
/etc/init.d/S50tera-api start || true |
|
||||
cp "$backup/sessiond" /usr/bin/tera-sessiond.restore |
|
||||
chmod 0755 /usr/bin/tera-sessiond.restore |
|
||||
mv /usr/bin/tera-sessiond.restore /usr/bin/tera-sessiond |
|
||||
/etc/init.d/S55tera-sessiond start || true |
|
||||
/etc/init.d/S61teraweb start || true |
|
||||
echo REPLACEMENT_DEFAULT_PORT_ROLLED_BACK |
|
||||
} |
|
||||
trap rollback EXIT |
|
||||
/etc/init.d/S55tera-sessiond stop |
|
||||
/etc/init.d/S43tera-netd stop |
|
||||
/etc/init.d/S61teraweb stop |
|
||||
/etc/init.d/S50tera-api stop |
|
||||
/etc/init.d/S40tera-seriald stop |
|
||||
/etc/init.d/S47tera-alarmd stop |
|
||||
/etc/init.d/S45tera-timed stop |
|
||||
/etc/init.d/S44tera-logd stop |
|
||||
sleep 2 |
|
||||
mv /usr/bin/tera-netd.next /usr/bin/tera-netd |
|
||||
/etc/init.d/S43tera-netd start |
|
||||
mv /usr/bin/tera-sessiond.next /usr/bin/tera-sessiond |
|
||||
mv /usr/bin/tera-api.next /usr/bin/tera-api |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
mv /usr/bin/tera-timed.next /usr/bin/tera-timed |
|
||||
mv /usr/bin/tera-alarmd.next /usr/bin/tera-alarmd |
|
||||
mv /usr/bin/tera-seriald.next /usr/bin/tera-seriald |
|
||||
mv /usr/bin/tera-logd.next /usr/bin/tera-logd |
|
||||
/etc/init.d/S44tera-logd start |
|
||||
/etc/init.d/S40tera-seriald start |
|
||||
/etc/init.d/S45tera-timed start |
|
||||
/etc/init.d/S47tera-alarmd start |
|
||||
( |
|
||||
set -a |
|
||||
. /etc/tera/tera-api.env |
|
||||
set +a |
|
||||
start-stop-daemon -S -q -b -m -p /run/tera-api.pid -c tera:tera -x /bin/sh -- -c 'exec /usr/bin/tera-api > /run/tera-api/replacement-startup.log 2>&1' |
|
||||
) |
|
||||
/etc/init.d/S55tera-sessiond start |
|
||||
/etc/init.d/S61teraweb start |
|
||||
ready=0 |
|
||||
for n in $(seq 1 120); do |
|
||||
if wget -q -T 2 -O "$stage/status" http://127.0.0.1/login/backend-status; then |
|
||||
if grep -q '"ready":true' "$stage/status" && grep -q '"completed":12' "$stage/status"; then ready=1; break; fi |
|
||||
fi |
|
||||
if [ $((n % 15)) = 0 ]; then tail -3 /run/tera-api/replacement-startup.log; fi |
|
||||
sleep 1 |
|
||||
done |
|
||||
cat "$stage/status" |
|
||||
if [ "$ready" != 1 ]; then tail -40 /run/tera-api/replacement-startup.log; exit 1; fi |
|
||||
wget -q -T 3 -O "$stage/login.js" http://127.0.0.1/static/js/login.js |
|
||||
grep -q '/login/bootstrap-time' "$stage/login.js" |
|
||||
grep -q '^PORT=80$' /etc/tera/teraweb.env |
|
||||
sha256sum /usr/bin/tera-sessiond /usr/bin/tera-netd /usr/bin/tera-api /usr/bin/tera-clone-demo-web /usr/bin/tera-timed /usr/bin/tera-alarmd /usr/bin/tera-seriald /usr/bin/tera-logd |
|
||||
sync |
|
||||
trap - EXIT |
|
||||
echo REPLACEMENT_DEFAULT_PORT_DEPLOY_OK |
|
||||
'@ |
|
||||
$script = $script.Replace('__APIHASH__', $hashes['/api']) |
|
||||
$script = $script.Replace('__WEBHASH__', $hashes['/web']) |
|
||||
$script = $script.Replace('__TIMEDHASH__', $hashes['/timed']) |
|
||||
$script = $script.Replace('__ALARMDHASH__', $hashes['/alarmd']) |
|
||||
$script = $script.Replace('__SERIALDHASH__', $hashes['/seriald']) |
|
||||
$script = $script.Replace('__LOGDHASH__', $hashes['/logd']) |
|
||||
$script = $script.Replace('__NETDHASH__', $hashes['/netd']) |
|
||||
$script = $script.Replace('__SESSIONDHASH__', $hashes['/sessiond']) |
|
||||
$script = $script.Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-replacement-default-port-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One' |
|
||||
$serial.Encoding = [Text.Encoding]::UTF8 |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-replacement-default-port.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-replacement-default-port.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(360) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
$route = $context.Request.Url.AbsolutePath |
|
||||
if ($route -eq '/deploy.sh') { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
elseif ($assets.ContainsKey($route)) { $bytes = [IO.File]::ReadAllBytes($assets[$route]) } |
|
||||
else { $context.Response.StatusCode = 404 } |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('REPLACEMENT_DEFAULT_PORT_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } |
|
||||
if ($output.Contains('REPLACEMENT_DEFAULT_PORT_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('REPLACEMENT_DEFAULT_PORT_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
foreach ($hash in $hashes.Values) { if (-not $output.Contains($hash)) { throw 'A deployed hash was not verified.' } } |
|
||||
Write-Output 'VERIFIED replacement device deployment on HTTP port 80' |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,96 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/startup-api-transfer.log') -Force | Out-Null |
|
||||
$binary = Join-Path $project 'build/linux-arm64/tera-clone-demo-api-startup' |
|
||||
$hash = (Get-FileHash -LiteralPath $binary -Algorithm SHA256).Hash.ToLowerInvariant() |
|
||||
$script = @' |
|
||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-startup-api-20260909 |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -O "$stage/api" http://192.168.0.100:9001/api |
|
||||
echo '__HASH__ '"$stage/api" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/startup-api-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-clone-demo-api "$backup/tera-clone-demo-api" |
|
||||
echo "BACKUP=$backup" |
|
||||
cp "$stage/api" /usr/bin/tera-clone-demo-api.next |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.next |
|
||||
rollback() { |
|
||||
trap - EXIT |
|
||||
/etc/init.d/S51tera-clone-demo-api stop || true |
|
||||
cp "$backup/tera-clone-demo-api" /usr/bin/tera-clone-demo-api.restore |
|
||||
chmod 0755 /usr/bin/tera-clone-demo-api.restore |
|
||||
mv /usr/bin/tera-clone-demo-api.restore /usr/bin/tera-clone-demo-api |
|
||||
/etc/init.d/S51tera-clone-demo-api start |
|
||||
echo STARTUP_API_ROLLED_BACK |
|
||||
} |
|
||||
trap rollback EXIT |
|
||||
/etc/init.d/S51tera-clone-demo-api stop |
|
||||
sleep 2 |
|
||||
mv /usr/bin/tera-clone-demo-api.next /usr/bin/tera-clone-demo-api |
|
||||
/etc/init.d/S51tera-clone-demo-api start |
|
||||
ready=0 |
|
||||
for n in $(seq 1 30); do |
|
||||
if wget -q -T 2 -O "$stage/status" http://127.0.0.1:8084/api/v1/auth/login-policy; then ready=1; break; fi |
|
||||
sleep 1 |
|
||||
done |
|
||||
[ "$ready" = 1 ] |
|
||||
sleep 3 |
|
||||
/etc/init.d/S51tera-clone-demo-api status |
|
||||
wget -q -T 3 -O "$stage/status" http://127.0.0.1:8084/api/v1/auth/login-policy |
|
||||
cat "$stage/status" |
|
||||
sha256sum /usr/bin/tera-clone-demo-api |
|
||||
sync |
|
||||
trap - EXIT |
|
||||
echo STARTUP_API_DEPLOY_OK |
|
||||
'@ |
|
||||
$script = $script.Replace('__HASH__', $hash).Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-startup-api-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM5',115200,'None',8,'One' |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-startup-api.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-startup-api.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(90) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
switch ($context.Request.Url.AbsolutePath) { |
|
||||
'/deploy.sh' { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
'/api' { $bytes = [IO.File]::ReadAllBytes($binary) } |
|
||||
default { $context.Response.StatusCode = 404 } |
|
||||
} |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('STARTUP_API_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('STARTUP_API_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
if (-not $output.Contains($hash)) { throw 'Deployed hash missing from console output.' } |
|
||||
Write-Output "VERIFIED deployed SHA256=$hash and API login-policy endpoint" |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,56 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/static-progress-transfer.log') -Force | Out-Null |
|
||||
$assets = @{'/web'=(Join-Path $project 'build/linux-arm64/tera-clone-demo-web')} |
|
||||
$hashes = @{} |
|
||||
foreach ($key in $assets.Keys) {$hashes[$key]=(Get-FileHash $assets[$key] -Algorithm SHA256).Hash.ToLowerInvariant()} |
|
||||
$script=[IO.File]::ReadAllText((Join-Path $PSScriptRoot 'deploy-static-progress.sh')).Replace('__WEBHASH__',$hashes['/web']).Replace("`r`n","`n") |
|
||||
$rule = 'TERA-replacement-default-port-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One' |
|
||||
$serial.Encoding = [Text.Encoding]::UTF8 |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-replacement-default-port.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-replacement-default-port.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(360) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
$route = $context.Request.Url.AbsolutePath |
|
||||
if ($route -eq '/deploy.sh') { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
elseif ($assets.ContainsKey($route)) { $bytes = [IO.File]::ReadAllBytes($assets[$route]) } |
|
||||
else { $context.Response.StatusCode = 404 } |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('REPLACEMENT_DEFAULT_PORT_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } |
|
||||
if ($output.Contains('REPLACEMENT_DEFAULT_PORT_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('REPLACEMENT_DEFAULT_PORT_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
foreach ($hash in $hashes.Values) { if (-not $output.Contains($hash)) { throw 'A deployed hash was not verified.' } } |
|
||||
Write-Output 'VERIFIED replacement device deployment on HTTP port 80' |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
@ -1,40 +0,0 @@ |
|||||
#!/bin/sh |
|
||||
set -eu |
|
||||
stage=/tmp/tera-static-progress |
|
||||
mkdir -p "$stage" |
|
||||
wget -q -T 30 -O "$stage/web" http://192.168.0.100:9001/web |
|
||||
echo '__WEBHASH__ '"$stage/web" | sha256sum -c - |
|
||||
backup=/userdata/tera-backups/static-progress-$(date +%Y%m%d-%H%M%S) |
|
||||
mkdir -p "$backup" |
|
||||
cp -p /usr/bin/tera-clone-demo-web "$backup/web" |
|
||||
echo "BACKUP=$backup" |
|
||||
rollback() { |
|
||||
trap - EXIT |
|
||||
/etc/init.d/S61teraweb stop || true |
|
||||
cp "$backup/web" /usr/bin/tera-clone-demo-web.restore |
|
||||
chmod 755 /usr/bin/tera-clone-demo-web.restore |
|
||||
mv /usr/bin/tera-clone-demo-web.restore /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S61teraweb start || true |
|
||||
echo REPLACEMENT_DEFAULT_PORT_ROLLED_BACK |
|
||||
} |
|
||||
trap rollback EXIT |
|
||||
cp "$stage/web" /usr/bin/tera-clone-demo-web.next |
|
||||
chmod 755 /usr/bin/tera-clone-demo-web.next |
|
||||
/etc/init.d/S61teraweb stop |
|
||||
mv /usr/bin/tera-clone-demo-web.next /usr/bin/tera-clone-demo-web |
|
||||
/etc/init.d/S61teraweb start |
|
||||
ready=0 |
|
||||
for n in $(seq 1 30); do |
|
||||
if wget -q -T 2 -O "$stage/status" http://127.0.0.1/login/backend-status && grep -q '"ready":true' "$stage/status"; then ready=1; break; fi |
|
||||
sleep 1 |
|
||||
done |
|
||||
[ "$ready" = 1 ] |
|
||||
wget -q -T 3 -O "$stage/style" http://127.0.0.1/static/css/style.css |
|
||||
! grep -q 'login-progress-slide' "$stage/style" |
|
||||
wget -q -T 3 -O "$stage/login" http://127.0.0.1/login |
|
||||
grep -q '20260909-static-progress-1' "$stage/login" |
|
||||
cat "$stage/status" |
|
||||
sha256sum /usr/bin/tera-clone-demo-web |
|
||||
sync |
|
||||
trap - EXIT |
|
||||
echo REPLACEMENT_DEFAULT_PORT_DEPLOY_OK |
|
||||
@ -1,59 +0,0 @@ |
|||||
$ErrorActionPreference = 'Stop' |
|
||||
$project = Split-Path -Parent $PSScriptRoot |
|
||||
Start-Transcript -Path (Join-Path $project 'runtime/restore-device-settings-transfer.log') -Force | Out-Null |
|
||||
$assets = @{} |
|
||||
$assets['/helper'] = Join-Path $project 'build/linux-arm64/tera-device-settings-tool' |
|
||||
$assets['/serial-env'] = Join-Path $project 'tera-seriald-cr.env' |
|
||||
$hashes = @{} |
|
||||
foreach ($key in $assets.Keys) { $hashes[$key] = (Get-FileHash -LiteralPath $assets[$key] -Algorithm SHA256).Hash.ToLowerInvariant() } |
|
||||
$script = [IO.File]::ReadAllText((Join-Path $project 'build/device-settings-tool/restore.sh')) |
|
||||
$script = $script.Replace('__HELPER__', $hashes['/helper']).Replace('__ENV__', $hashes['/serial-env']).Replace("`r`n", "`n") + "`n" |
|
||||
$rule = 'TERA-restore-device-settings-' + [guid]::NewGuid().ToString('N') |
|
||||
$listener = New-Object System.Net.HttpListener |
|
||||
$listener.Prefixes.Add('http://192.168.0.100:9001/') |
|
||||
$serial = New-Object System.IO.Ports.SerialPort 'COM4',115200,'None',8,'One' |
|
||||
$serial.Encoding = [Text.Encoding]::UTF8 |
|
||||
try { |
|
||||
New-NetFirewallRule -DisplayName $rule -Direction Inbound -Action Allow -Protocol TCP -LocalPort 9001 -RemoteAddress 192.168.0.223 | Out-Null |
|
||||
$listener.Start() |
|
||||
$serial.Open() |
|
||||
$serial.WriteLine('wget -q -O /tmp/deploy-restore-device-settings.sh http://192.168.0.100:9001/deploy.sh && sh /tmp/deploy-restore-device-settings.sh') |
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(360) |
|
||||
$output = '' |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
while ([DateTime]::UtcNow -lt $deadline) { |
|
||||
if ($pending.IsCompleted) { |
|
||||
$context = $pending.GetAwaiter().GetResult() |
|
||||
if ($context.Request.RemoteEndPoint.Address.ToString() -ne '192.168.0.223') { |
|
||||
$context.Response.StatusCode = 403 |
|
||||
$context.Response.Close() |
|
||||
} else { |
|
||||
$bytes = $null |
|
||||
$route = $context.Request.Url.AbsolutePath |
|
||||
if ($route -eq '/deploy.sh') { $bytes = [Text.Encoding]::UTF8.GetBytes($script) } |
|
||||
elseif ($assets.ContainsKey($route)) { $bytes = [IO.File]::ReadAllBytes($assets[$route]) } |
|
||||
else { $context.Response.StatusCode = 404 } |
|
||||
if ($null -ne $bytes) { |
|
||||
$context.Response.ContentLength64 = $bytes.Length |
|
||||
$context.Response.OutputStream.Write($bytes, 0, $bytes.Length) |
|
||||
} |
|
||||
$context.Response.Close() |
|
||||
} |
|
||||
$pending = $listener.GetContextAsync() |
|
||||
} |
|
||||
$chunk = $serial.ReadExisting() |
|
||||
if ($chunk) { $output += $chunk; Write-Host -NoNewline $chunk } |
|
||||
if ($output.Contains('RESTORE_DEVICE_SETTINGS_ROLLED_BACK')) { throw 'Readiness failed; old pair restored.' } |
|
||||
if ($output.Contains('RESTORE_DEVICE_SETTINGS_DEPLOY_OK')) { break } |
|
||||
Start-Sleep -Milliseconds 100 |
|
||||
} |
|
||||
if (-not $output.Contains('RESTORE_DEVICE_SETTINGS_DEPLOY_OK')) { throw 'Deployment did not report success; inspect device console.' } |
|
||||
foreach ($hash in $hashes.Values) { if (-not $output.Contains($hash)) { throw 'A deployed hash was not verified.' } } |
|
||||
Write-Output 'VERIFIED replacement device deployment on HTTP port 80' |
|
||||
} finally { |
|
||||
if ($serial.IsOpen) { $serial.Close() } |
|
||||
$serial.Dispose() |
|
||||
$listener.Close() |
|
||||
Remove-NetFirewallRule -DisplayName $rule -ErrorAction SilentlyContinue |
|
||||
Stop-Transcript | Out-Null |
|
||||
} |
|
||||
Loading…
Reference in new issue