Production Line Installation (factory.sh)
Used when provisioning 100–1000 boxes on a production line. The operator fills in a single screen of prompts, and everything — EDGE_ID / hostname / credentials / Docker / cosign verify / container startup — happens automatically. 30 seconds per box.
For a single box / field installation (SI staff), read the Quick Install (One-Liner) page first. Production line vs. field installation differ only in the entry point; the result is identical.
0. At a Glance
sudo -i
git clone https://dev.kopens.io/plantpulse-edge/plantpulse-edge-docker.git \
/home/kopens/plantpulse-edge-docker
cd /home/kopens/plantpulse-edge-docker/bin
sudo bash factory.sh
factory.sh automatically does the following:
- Auto-detects the LAN1 NIC MAC — generates
EDGE_<HEX6>from the last 6 characters of the MAC (e.g.EDGE_3F8A21). - Syncs the hostname —
hostnameis also changed toedge-<hex6>(RFC 1123, lowercase + hyphens). - 5-field prompt — SITE_ID / SERVER_HOST / SERVER_API_KEY / LAN1 NIC / LAN2 NIC.
- Delegates to
install.sh— Docker / cosign / random secret / app.properties / container startup. - Prints factory-label.txt — for attaching to the box. Print, then seal.
- Appends to factory-inventory.csv — collect at the end of the line → upload to the in-house inventory DB.
1. Automatic ID / hostname Generation
EDGE_ID — EDGE_<HEX6>
LAN1 NIC eth1 MAC = 00:1a:2b:3f:8a:21
─────
마지막 6 자 (HEX6) = 3F8A21
EDGE_ID = EDGE_3F8A21
hostname = edge-3f8a21
Why MAC?
- The hostname can change with every OS install (cloud-init /
hostnamectl) and may collide - The MAC is burned into the hardware, guaranteeing uniqueness per box
- Last 6 characters of the 24-bit portion → roughly 16.7 million combinations. For a 1000-unit production run the collision probability is ~0.003% (safe by the birthday paradox)
Domain rules (CLAUDE.md) are enforced:
| Identifier | Regex | Example |
|---|---|---|
EDGE_ID | ^EDGE_[A-Z0-9_]{1,60}$ | EDGE_3F8A21 |
SITE_ID | ^SITE_[A-Z0-9_]{1,60}$ | SITE_FAB1_042 |
| hostname (RFC 1123) | lowercase + hyphens | edge-3f8a21 |
factory.sh proceeds only after validating against the regex above — if it fails, it aborts immediately.
MAC Collisions / Boxes Without a NIC
- Possible collision — if the same EDGE_ID already exists in
/data1/factory-inventory.csv, it aborts immediately (preventing silent duplicates). Automatically detects provisioning the same box twice or a MAC collision. - Boxes without a MAC (virtual NICs, etc.) —
derive_edge_id_from_macfails → install.sh falls back to a hostname-based value. Almost never happens on the line.
2. The 5 Prompt Fields
Entered by the operator on the line:
══════════════════════════════════════════════════════════════════════
PlantPulse Edge — Factory Provisioning
══════════════════════════════════════════════════════════════════════
Detected NIC eth1 MAC : 00:1a:2b:3f:8a:21
Auto EDGE_ID : EDGE_3F8A21
Auto hostname : edge-3f8a21
══════════════════════════════════════════════════════════════════════
Site ID (^SITE_[A-Z0-9_]+) [SITE_00001]: SITE_FAB1_042
Platform server host [192.168.0.41]: 10.10.0.5
Platform API key []: kFa7bGz8pQrN3xMv9wT2sLcEjY1uV6oH
LAN1 NIC (PLC) [eth1]: ← Enter = default
LAN2 NIC (WAN) [eth2]: ← Enter = default
══════════════════════════════════════════════════════════════════════
Summary
EDGE_ID : EDGE_3F8A21
hostname : edge-3f8a21
SITE_ID : SITE_FAB1_042
SERVER_HOST : 10.10.0.5
SERVER_API_KEY : kFa7…(32 chars)
LAN1 NIC (PLC) : eth1
LAN2 NIC (WAN) : eth2
══════════════════════════════════════════════════════════════════════
Proceed with install.sh? [y/N]
Press y and install.sh runs for 5–10 minutes (including the initial 3 GB image pull), then prints the label.
3. Auto-Generated Credentials (G2)
install.sh generates a different random password for each box (details in README — Secrets):
| Variable | Length | Location |
|---|---|---|
ADMIN_PASS (UI login) | random 14 chars + ! | /etc/kopens/credentials.txt |
API_KEY (REST) | random 16 chars | file above + env.sh.generated |
MQTT_PASS (HiveMQ) | random 14 chars + ! | file above |
OPCUA_PASS | random 14 chars + ! | file above |
CERT_PASS (SSL keystore) | random 14 chars + ! | file above |
/etc/kopens/env.sh.generated (chmod 0600 root) is the sidecar sourced by all subsequent scripts (upgrade.sh / restart.sh) — the same credentials persist across reboots.
4. Image Signature Verification (G3)
install.sh automatically installs cosign, places cosign.pub, and verifies automatically right after docker pull.
[install] Installing cosign ...
[install] OK — cosign installed (cosign version 2.4.1)
[install] cosign.pub installed — image verification enabled
[install] docker pull — docker.kopens.io/pe/plantpulse-edge:2026-20260523
[install] OK — Pull complete
[cosign] OK — signature verified for docker.kopens.io/pe/plantpulse-edge:2026-20260523
If cosign.pub is still a placeholder (the operations team has not generated a keypair), only a WARN is emitted and installation continues. See the Cosign chapter (README).
5. Label + Inventory
/etc/kopens/factory-label.txt — For Attaching to the Box
When installation finishes it is printed to the screen and saved to chmod 0600. Line staff print it on a label printer and seal the box:
══════════════════════════════════════════════════════════════════════
PlantPulse Edge — Factory Label
══════════════════════════════════════════════════════════════════════
EDGE_ID : EDGE_3F8A21
hostname : edge-3f8a21
SITE_ID : SITE_FAB1_042
build_date : 2026-05-19 14:25:33
──────────────────────────────────────────────────────────────────────
LAN1 (PLC) : eth1 MAC=00:1a:2b:3f:8a:21
LAN2 (WAN) : eth2 MAC=00:1a:2b:3f:8a:22
──────────────────────────────────────────────────────────────────────
UI admin : admin / aBcD1234EfGh!
REST API : kFa7bGz8pQrN3xMv9wT2sLcEjY1uV6oH
(full credentials → /etc/kopens/credentials.txt — root only)
══════════════════════════════════════════════════════════════════════
/data1/factory-inventory.csv — For Collection at the Line
One line is appended per box. The operator collects it at the end of the line → uploads to the in-house inventory DB:
edge_id,hostname,site_id,server_host,lan1_iface,lan1_mac,lan2_iface,lan2_mac,build_date,iso8601
EDGE_3F8A21,edge-3f8a21,SITE_FAB1_042,10.10.0.5,eth1,00:1a:2b:3f:8a:21,eth2,00:1a:2b:3f:8a:22,2026-05-19 14:25:33,2026-05-19T05:25:33Z
EDGE_5B7C03,edge-5b7c03,SITE_FAB1_043,10.10.0.5,eth1,00:1a:2b:5b:7c:03,eth2,00:1a:2b:5b:7c:04,2026-05-19 14:28:12,2026-05-19T05:28:12Z
6. Bulk Collection at the End of the Line
# 라인 마지막 박스 처리 후 회수 워크스테이션에서
scp root@edge-3f8a21:/data1/factory-inventory.csv ./inventory-line1-$(date +%F).csv
# 회수한 CSV 들을 사내 inventory DB 로 업로드 (예: scripts/import-inventory.py)
python3 import-inventory.py inventory-line1-2026-05-19.csv
Alternatively, a hook can be added to push automatically to the platform the moment the box powers on (in Phase 1, when the in-house provisioning REST API is introduced).
7. Common Pitfalls
| Symptom | Cause | Response |
|---|---|---|
EDGE_ID '...' fails ^EDGE_[A-Z0-9_]{1,60}$ | The LAN1 NIC is a virtual NIC whose MAC is not ASCII | Specify explicitly, as in LAN1_IFACE=enp1s0 bash factory.sh |
already_in_inventory: EDGE_3F8A21 already provisioned | Re-run on the same box, or a MAC collision | Remove the corresponding line with vi /data1/factory-inventory.csv and re-run. If it is a MAC collision, stop the line and report |
cosign verify FAILED (COSIGN_REQUIRE=1) | Image is unsigned OR cosign.pub is wrong | Check whether the build server missed the sign-image.sh call; refresh cosign.pub |
hostnamectl set-hostname failed | Run inside a container / insufficient privileges | factory.sh requires root — sudo bash factory.sh |
| Prompt does not appear (label not printed) | install.sh failed mid-way | Check journalctl -u plantpulse-edge.service -n 50, then see Troubleshooting |