Skip to main content

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.

This page is the production line SOP

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:

  1. Auto-detects the LAN1 NIC MAC — generates EDGE_<HEX6> from the last 6 characters of the MAC (e.g. EDGE_3F8A21).
  2. Syncs the hostnamehostname is also changed to edge-<hex6> (RFC 1123, lowercase + hyphens).
  3. 5-field prompt — SITE_ID / SERVER_HOST / SERVER_API_KEY / LAN1 NIC / LAN2 NIC.
  4. Delegates to install.sh — Docker / cosign / random secret / app.properties / container startup.
  5. Prints factory-label.txt — for attaching to the box. Print, then seal.
  6. 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:

IdentifierRegexExample
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 + hyphensedge-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_mac fails → 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):

VariableLengthLocation
ADMIN_PASS (UI login)random 14 chars + !/etc/kopens/credentials.txt
API_KEY (REST)random 16 charsfile above + env.sh.generated
MQTT_PASS (HiveMQ)random 14 chars + !file above
OPCUA_PASSrandom 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

SymptomCauseResponse
EDGE_ID '...' fails ^EDGE_[A-Z0-9_]{1,60}$The LAN1 NIC is a virtual NIC whose MAC is not ASCIISpecify explicitly, as in LAN1_IFACE=enp1s0 bash factory.sh
already_in_inventory: EDGE_3F8A21 already provisionedRe-run on the same box, or a MAC collisionRemove 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 wrongCheck whether the build server missed the sign-image.sh call; refresh cosign.pub
hostnamectl set-hostname failedRun inside a container / insufficient privilegesfactory.sh requires root — sudo bash factory.sh
Prompt does not appear (label not printed)install.sh failed mid-wayCheck journalctl -u plantpulse-edge.service -n 50, then see Troubleshooting