مقایسه کامیتها
3
کامیتها
9b5bff8002
...
fc45c88708
| مولف | SHA1 | تاریخ | |
|---|---|---|---|
|
|
fc45c88708 | ||
|
|
68583fd185 | ||
|
|
0528032639 |
@@ -22,7 +22,7 @@ jobs:
|
||||
CONFIRM_SQUASHFS_MTDBLOCK3: ${{ inputs.confirm_squashfs_mtdblock3 }}
|
||||
run: |
|
||||
if [ "$CONFIRM_SQUASHFS_MTDBLOCK3" = "true" ]; then
|
||||
/usr/bin/python3 scripts/serial_command.py boot-config --confirm-squashfs-mtdblock3
|
||||
/usr/bin/python3 deployment/serial/serial_command.py boot-config --confirm-squashfs-mtdblock3
|
||||
else
|
||||
/usr/bin/python3 scripts/serial_command.py boot-config
|
||||
/usr/bin/python3 deployment/serial/serial_command.py boot-config
|
||||
fi
|
||||
|
||||
@@ -12,4 +12,4 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Detect flash, set bootcmd and verify
|
||||
run: /usr/bin/python3 scripts/serial_command.py set-bootcmd
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py set-bootcmd
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
|
||||
- name: Transfer DTB, write SPI flash and verify read-back
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py transfer-dtb
|
||||
/usr/bin/python3 deployment/serial/serial_command.py transfer-dtb
|
||||
|
||||
@@ -38,41 +38,41 @@ jobs:
|
||||
PY
|
||||
|
||||
- name: Check current U-Boot CLI
|
||||
run: /usr/bin/python3 scripts/serial_command.py check
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py check
|
||||
|
||||
- name: Configure and verify network
|
||||
env:
|
||||
UBOOT_DEVICE_IP: ${{ vars.UBOOT_DEVICE_IP || '192.168.75.100' }}
|
||||
TFTP_SERVER_IP: ${{ vars.TFTP_SERVER_IP || '192.168.75.247' }}
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py configure --device-ip "$UBOOT_DEVICE_IP" --tftp-server-ip "$TFTP_SERVER_IP"
|
||||
/usr/bin/python3 deployment/serial/serial_command.py configure --device-ip "$UBOOT_DEVICE_IP" --tftp-server-ip "$TFTP_SERVER_IP"
|
||||
|
||||
- name: Check all firmware sizes against detected flash layout
|
||||
run: /usr/bin/python3 scripts/serial_command.py preflight
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py preflight
|
||||
|
||||
- name: Erase detected SPI flash and verify sample
|
||||
run: /usr/bin/python3 scripts/serial_command.py erase
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py erase
|
||||
|
||||
- name: Transfer, write and verify U-Boot
|
||||
run: /usr/bin/python3 scripts/serial_command.py transfer
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py transfer
|
||||
|
||||
- name: Transfer, write and verify DTB
|
||||
run: /usr/bin/python3 scripts/serial_command.py transfer-dtb
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py transfer-dtb
|
||||
|
||||
- name: Transfer, write and verify Kernel
|
||||
run: /usr/bin/python3 scripts/serial_command.py transfer-kernel
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py transfer-kernel
|
||||
|
||||
- name: Transfer, write and verify RootFS
|
||||
run: /usr/bin/python3 scripts/serial_command.py transfer-rootfs
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py transfer-rootfs
|
||||
|
||||
- name: Inspect boot configuration
|
||||
run: /usr/bin/python3 scripts/serial_command.py boot-config
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py boot-config
|
||||
|
||||
- name: Configure and verify bootcmd for detected flash
|
||||
run: /usr/bin/python3 scripts/serial_command.py set-bootcmd
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py set-bootcmd
|
||||
|
||||
- name: Save U-Boot environment before reboot
|
||||
run: /usr/bin/python3 scripts/serial_command.py saveenv
|
||||
# - name: Save U-Boot environment before reboot
|
||||
# run: /usr/bin/python3 deployment/serial/serial_command.py saveenv
|
||||
|
||||
- name: Reboot hardware after successful flashing
|
||||
run: /usr/bin/python3 scripts/serial_command.py reboot
|
||||
run: /usr/bin/python3 deployment/serial/serial_command.py reboot
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
|
||||
- name: Erase entire flash and verify first 256 bytes
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py erase
|
||||
/usr/bin/python3 deployment/serial/serial_command.py erase
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
name: init test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
init-test:
|
||||
runs-on: serial-runner
|
||||
steps:
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Verify ordered boot and Linux console
|
||||
env:
|
||||
RUTBS_PASSWORD: ${{ secrets.RUTBS_PASSWORD }}
|
||||
run: /usr/bin/python3 deployment/serial/init_test.py
|
||||
|
||||
- name: Upload redacted serial log
|
||||
if: ${{ always() }}
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: boot-log
|
||||
path: artifacts/boot.log
|
||||
if-no-files-found: error
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
|
||||
- name: Transfer Kernel, write SPI flash and verify read-back
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py transfer-kernel
|
||||
/usr/bin/python3 deployment/serial/serial_command.py transfer-kernel
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
|
||||
- name: Detect flash, transfer RootFS and verify read-back
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py transfer-rootfs
|
||||
/usr/bin/python3 deployment/serial/serial_command.py transfer-rootfs
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
|
||||
- name: Check current U-Boot CLI prompt
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py check
|
||||
/usr/bin/python3 deployment/serial/serial_command.py check
|
||||
|
||||
@@ -27,4 +27,4 @@ jobs:
|
||||
TEST_HOST_IP: ${{ vars.TEST_HOST_IP || '192.168.75.15' }}
|
||||
TEST_DEVICE_IP: ${{ vars.TEST_DEVICE_IP }}
|
||||
run: |
|
||||
/usr/bin/python3 scripts/software_test.py --interface "$TEST_INTERFACE" --host-ip "$TEST_HOST_IP" --device-ip "$TEST_DEVICE_IP"
|
||||
/usr/bin/python3 deployment/serial/software_test.py --interface "$TEST_INTERFACE" --host-ip "$TEST_HOST_IP" --device-ip "$TEST_DEVICE_IP"
|
||||
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
UBOOT_DEVICE_IP: ${{ vars.UBOOT_DEVICE_IP || '192.168.75.100' }}
|
||||
TFTP_SERVER_IP: ${{ vars.TFTP_SERVER_IP || '192.168.75.247' }}
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py configure --device-ip "$UBOOT_DEVICE_IP" --tftp-server-ip "$TFTP_SERVER_IP"
|
||||
/usr/bin/python3 deployment/serial/serial_command.py configure --device-ip "$UBOOT_DEVICE_IP" --tftp-server-ip "$TFTP_SERVER_IP"
|
||||
|
||||
@@ -13,4 +13,4 @@ jobs:
|
||||
|
||||
- name: Transfer firmware, write SPI flash and verify read-back
|
||||
run: |
|
||||
/usr/bin/python3 scripts/serial_command.py transfer
|
||||
/usr/bin/python3 deployment/serial/serial_command.py transfer
|
||||
|
||||
+40
-2
@@ -21,7 +21,7 @@ workflow تنظیم شبکه و workflow اصلی فلش از آنها است
|
||||
نمونهٔ اجرای مستقیم:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 scripts/serial_command.py configure \
|
||||
/usr/bin/python3 deployment/serial/serial_command.py configure \
|
||||
--device-ip 192.168.75.100 \
|
||||
--tftp-server-ip 192.168.75.247
|
||||
```
|
||||
@@ -53,10 +53,48 @@ IP میزبان باید از قبل روی کارت شبکهٔ انتخاب
|
||||
نمونهٔ اجرای دستی روی runner با جایگزینکردن IP واقعی دستگاه:
|
||||
|
||||
```bash
|
||||
/usr/bin/python3 scripts/software_test.py \
|
||||
/usr/bin/python3 deployment/serial/software_test.py \
|
||||
--interface eno1 \
|
||||
--host-ip 192.168.75.15 \
|
||||
--device-ip <DEVICE_IP>
|
||||
```
|
||||
|
||||
برای محل فایلهای firmware و نقشهٔ فلش، [راهنمای firmware](firmware/README.md) را ببینید.
|
||||
|
||||
## ساختار استقرار
|
||||
|
||||
اسکریپتهای اجرایی در `deployment/serial/` قرار دارند:
|
||||
|
||||
- `serial_command.py`: کنترل U-Boot، فلش و راهاندازی برد از طریق سریال.
|
||||
- `software_test.py`: بررسی پاسخ ping دستگاه پس از بوت.
|
||||
|
||||
workflowهای اصلی امینپرداز و ورودیهای workflowهای کمکی در
|
||||
`.gitea/workflows/` باقی میمانند و اسکریپتها را از مسیر جدید اجرا میکنند.
|
||||
فایلهای باینری همچنان در پوشهٔ `firmware/` ریشهٔ مخزن قرار میگیرند.
|
||||
|
||||
## تست ترتیب بوت از طریق Serial
|
||||
|
||||
workflow دستی `init test` از فایل `.gitea/workflows/init-test.yml` اجرا میشود.
|
||||
رمز دستگاه را با نام `RUTBS_PASSWORD` در **Actions → Secrets** مخزن تعریف کنید؛
|
||||
رمز در سورس یا متغیرهای عمومی قرار نمیگیرد. رانر به `pyserial` و دسترسی
|
||||
به پورت `/dev/ttyUSB0` نیاز دارد.
|
||||
|
||||
برای اجرای مستقیم، رمز را بدون نمایش در ترمینال دریافت کنید:
|
||||
|
||||
```bash
|
||||
read -rs -p 'RUTBS password: ' RUTBS_PASSWORD
|
||||
export RUTBS_PASSWORD
|
||||
python3 deployment/serial/init_test.py
|
||||
unset RUTBS_PASSWORD
|
||||
```
|
||||
|
||||
برای پورت دیگر، گزینهٔ `--port /dev/ttyUSB1` را اضافه کنید.
|
||||
هنگام اجرای تست، برد باید در پرامپت U-Boot یعنی `=>` باشد.
|
||||
اسکریپت ابتدا با ارسال Enter پرامپت را تازه میکند و پس از تشخیص آن،
|
||||
دستور `res` را برای ریبوت میفرستد؛ سپس مراحل بوت را بهترتیب بررسی میکند.
|
||||
این تست باید پیامهای انتخاب سطح دیباگ و فعالسازی کنسول را ببیند و
|
||||
با workflow دیگری که همان پورت Serial را استفاده میکند همزمان اجرا نشود.
|
||||
از شروع تست، مهلت مشترک مراحل ۱۸۰ ثانیه است. تنها پس از ورود و دریافت
|
||||
خروجی دقیق `Linux` از `uname -s`، پیام `BOOT TEST: PASS` و کد خروج صفر
|
||||
برگردانده میشود. خطا کد خروج غیرصفر دارد. لاگ Serial با حذف رمز در
|
||||
`artifacts/boot.log` ذخیره و حتی در شکست تست بهصورت آرتیفکت بارگذاری میشود.
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Restart RUTBS from U-Boot and verify the ordered boot and kernel name."""
|
||||
import argparse
|
||||
import fcntl
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import serial
|
||||
|
||||
|
||||
STAGES = (
|
||||
('U-Boot prompt', '=>', b'res\r'),
|
||||
('kernel', 'Starting kernel ...', None),
|
||||
('debug selection', 'Press the [1], [2], [3] or [4] key and hit [enter] to select the debug level', None),
|
||||
('console activation', 'Please press Enter to activate this console.', b'\r'),
|
||||
('login', 'RUTBS login:', b'root\r'),
|
||||
('password', 'Password:', 'password'),
|
||||
('shell', 'root@RUTBS:~#', b'uname -s\r'),
|
||||
('command result', 'root@RUTBS:~#', None),
|
||||
)
|
||||
FAILURE = re.compile(r'Kernel panic|Login incorrect|Login failed|Authentication fail(?:ed|ure)|Access denied', re.I)
|
||||
|
||||
|
||||
def run_test(port, password, transcript, deadline):
|
||||
stage = 0
|
||||
pending = ''
|
||||
observed = ''
|
||||
# Request a fresh prompt when the existing => predates opening the port.
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise RuntimeError('Overall test timeout')
|
||||
port.write_timeout = min(2, remaining)
|
||||
if port.write(b'\r') != 1:
|
||||
raise RuntimeError('Incomplete serial write')
|
||||
while time.monotonic() < deadline:
|
||||
data = port.read(min(port.in_waiting or 1, 4096))
|
||||
if not data:
|
||||
continue
|
||||
transcript.extend(data)
|
||||
chunk = data.decode('utf-8', errors='replace').replace('\r', '')
|
||||
pending += chunk
|
||||
observed = (observed + chunk)[-8192:]
|
||||
if FAILURE.search(observed):
|
||||
raise RuntimeError('Boot or authentication failure detected')
|
||||
while stage < len(STAGES):
|
||||
name, token, action = STAGES[stage]
|
||||
if stage == 0:
|
||||
match = re.search(r'(?:^|\n)[^\n]*=> *$', pending)
|
||||
position = pending.rfind('=>') if match else -1
|
||||
else:
|
||||
position = pending.find(token)
|
||||
if position < 0:
|
||||
break
|
||||
before = pending[:position]
|
||||
pending = pending[position + len(token):]
|
||||
if stage == len(STAGES) - 1:
|
||||
lines = [line.strip() for line in before.splitlines() if line.strip()]
|
||||
if lines and lines[0] == 'uname -s':
|
||||
lines.pop(0) # A terminal may echo the command.
|
||||
if lines != ['Linux']:
|
||||
raise RuntimeError('uname -s output was not exactly Linux')
|
||||
if time.monotonic() >= deadline:
|
||||
raise RuntimeError('Overall test timeout')
|
||||
return
|
||||
if action is not None:
|
||||
# Discard pre-existing input: the next prompt must follow our write.
|
||||
pending = ''
|
||||
payload = password.encode('utf-8') + b'\r' if action == 'password' else action
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining <= 0:
|
||||
raise RuntimeError('Overall test timeout')
|
||||
port.write_timeout = min(2, remaining)
|
||||
if port.write(payload) != len(payload):
|
||||
raise RuntimeError('Incomplete serial write')
|
||||
stage += 1
|
||||
if len(pending) > 65536:
|
||||
raise RuntimeError('Unexpected excessive serial output in current stage')
|
||||
raise RuntimeError('Timeout waiting for ' + STAGES[stage][0])
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--port', default='/dev/ttyUSB0')
|
||||
args = parser.parse_args()
|
||||
deadline = time.monotonic() + 180
|
||||
password = os.environ.get('RUTBS_PASSWORD', '')
|
||||
transcript = bytearray()
|
||||
result = 1
|
||||
reason = 'Serial connection or configuration error'
|
||||
log = Path('artifacts/boot.log')
|
||||
try:
|
||||
log.parent.mkdir(parents=True, exist_ok=True)
|
||||
# Never put raw serial bytes on disk or stdout, even during authentication.
|
||||
log.write_text('', encoding='utf-8')
|
||||
if not password or any(c in password for c in '\r\n\x00'):
|
||||
raise RuntimeError('RUTBS_PASSWORD is missing or invalid')
|
||||
with open('/tmp/uboot-serial.lock', 'w') as lock:
|
||||
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
||||
with serial.Serial(args.port, 115200, timeout=0.1, write_timeout=2, exclusive=True) as port:
|
||||
run_test(port, password, transcript, deadline)
|
||||
result = 0
|
||||
except RuntimeError as exc:
|
||||
reason = str(exc)
|
||||
except Exception:
|
||||
# Serial/library exceptions can include transmitted data; do not print them.
|
||||
pass
|
||||
finally:
|
||||
content = transcript.decode('utf-8', errors='replace').replace('\r', '')
|
||||
if password:
|
||||
content = content.replace(password, '[REDACTED]')
|
||||
reason = reason.replace(password, '[REDACTED]')
|
||||
try:
|
||||
log.write_text(content, encoding='utf-8')
|
||||
except OSError:
|
||||
result = 1
|
||||
reason = 'Could not save serial log'
|
||||
if result:
|
||||
print('BOOT TEST: FAIL')
|
||||
print(reason)
|
||||
else:
|
||||
print('BOOT TEST: PASS')
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -206,7 +206,7 @@ def boot_config(cli, confirmed=False):
|
||||
if not confirmed:
|
||||
print("\nBoot configuration inspected; no changes requested.")
|
||||
return
|
||||
bootargs = "console=ttyS0,115200 loglevel=8 root=/dev/mtdblock3 rootfstype=squashfs ro rootwait"
|
||||
bootargs = "console=ttyS0,115200 loglevel=1 root=/dev/mtdblock3 rootfstype=squashfs ro rootwait"
|
||||
cli.command("setenv bootargs '" + bootargs + "'")
|
||||
cli.command("printenv bootargs", r"^bootargs=" + re.escape(bootargs) + r"$")
|
||||
print("\nBootargs verified in RAM; environment not saved and board not rebooted.")
|
||||
@@ -224,7 +224,7 @@ def main():
|
||||
args = parser.parse_args()
|
||||
if args.firmware is None:
|
||||
filename = "rootfs.squashfs" if args.mode == "transfer-rootfs" else "zImage" if args.mode == "transfer-kernel" else ("sun8i-v3s-licheepi-zero-dock.dtb" if args.mode == "transfer-dtb" else "u-boot-sunxi-with-spl.bin")
|
||||
args.firmware = Path(__file__).resolve().parent.parent / "firmware" / filename
|
||||
args.firmware = Path(__file__).resolve().parents[2] / "firmware" / filename
|
||||
if args.mode in ("transfer", "transfer-dtb", "transfer-kernel", "transfer-rootfs") and not args.firmware.is_file():
|
||||
raise RuntimeError("Firmware file not found: " + str(args.firmware))
|
||||
# Shared by both workflows: fail rather than interleave serial commands.
|
||||
@@ -250,7 +250,7 @@ def main():
|
||||
files = {"uboot": "u-boot-sunxi-with-spl.bin", "dtb": "sun8i-v3s-licheepi-zero-dock.dtb",
|
||||
"kernel": "zImage", "rootfs": "rootfs.squashfs"}
|
||||
for part, filename in files.items():
|
||||
path = Path(__file__).resolve().parent.parent / "firmware" / filename
|
||||
path = Path(__file__).resolve().parents[2] / "firmware" / filename
|
||||
minimum = 64 if part in ("kernel", "rootfs") else 32
|
||||
limit = LAYOUTS[capacity][part][1]
|
||||
if not path.is_file() or not minimum <= path.stat().st_size <= limit:
|
||||
مرجع در شماره جدید
مسدود کردن کاربر