Skip to content

Commit 1c9be0e

Browse files
committed
Enhance manufacturing database and UI with OS image metadata
- Updated the devices table schema to include new fields for OS image filename and SHA256 hash, ensuring comprehensive tracking of provisioning data. - Modified the metadata gathering process to capture OS image details, including fallback mechanisms for SHA256 calculation. - Enhanced the manufacturing view to display OS image filename and SHA256, improving visibility of provisioning information. - Updated relevant documentation to reflect changes in the manufacturing database structure and data handling.
1 parent 3e56757 commit 1c9be0e

File tree

6 files changed

+146
-24
lines changed

6 files changed

+146
-24
lines changed

debian/postinst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ MFG_DB_SCHEMA="id integer primary key,
114114
eeprom_write_protected integer DEFAULT NULL,
115115
pubkey_programmed integer DEFAULT NULL,
116116
signed_boot_enabled integer DEFAULT NULL,
117+
os_image_filename varchar(255) DEFAULT NULL,
118+
os_image_sha256 char(64) DEFAULT NULL,
117119
provision_ts timestamp default current_timestamp"
118120

119121
# Ensure WAL journal mode
@@ -131,7 +133,7 @@ else
131133
CURRENT_COLUMNS=$(sqlite3 "$MFG_DB_PATH" "PRAGMA table_info(devices);" | awk -F'|' '{print $2}' | tr '\n' ',')
132134

133135
# Check if any expected columns are missing
134-
for COL in id boardname serial eth_mac wifi_mac bt_mac mmc_size mmc_cid rpi_duid board_revision processor memory manufacturer secure jtag_locked eeprom_write_protected pubkey_programmed signed_boot_enabled provision_ts; do
136+
for COL in id boardname serial eth_mac wifi_mac bt_mac mmc_size mmc_cid rpi_duid board_revision processor memory manufacturer secure jtag_locked eeprom_write_protected pubkey_programmed signed_boot_enabled os_image_filename os_image_sha256 provision_ts; do
135137
if ! echo "$CURRENT_COLUMNS" | grep -q "$COL"; then
136138
echo "Migration needed: column $COL is missing from manufacturing.db devices table"
137139

docs/api_endpoints.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ The endpoint returns a JSON array where each element represents a provisioned de
5151
"eeprom_write_protected": "1",
5252
"pubkey_programmed": "1",
5353
"signed_boot_enabled": "1",
54+
"os_image_filename": "raspios-2025-04-01.img",
55+
"os_image_sha256": "4f2d9c5b0e3b1d8a9c1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c",
5456
"provision_ts": "2025-04-28 13:53:28"
5557
},
5658
...

docs/config_vars.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Set to any value to enable EEPROM write protection.
110110
=== RPI_SB_PROVISIONER_MANUFACTURING_DB
111111
*Optional*
112112

113-
Store manufacturing data in a sqlite3 database. This will include the board serial, board revision, the boot ROM version, the MAC address of the ethernet port, any set hash of the customer signing key, the JTAG lock state, the board attributes and the advanced boot flags.
113+
Store manufacturing data in a sqlite3 database. This will include the board serial, board revision, the boot ROM version, the MAC address of the ethernet port, any set hash of the customer signing key, the JTAG lock state, the board attributes and the advanced boot flags. It will also include the OS image filename and its SHA256 used during provisioning.
114114

115115
You must not specify the path of a database stored on a network drive or similar storage, as this mechanism is only safe to use on a single provisioning system. For merging the output with multiple provisioning systems, consider <<../README.adoc#_processing_the_manufacturing_database,Processing the manufacturing database>> in the main documentation.
116116

host-support/manufacturing-data

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ metadata_gather() {
111111
eeprom_write_protected integer DEFAULT NULL,
112112
pubkey_programmed integer DEFAULT NULL,
113113
signed_boot_enabled integer DEFAULT NULL,
114+
os_image_filename varchar(255) DEFAULT NULL,
115+
os_image_sha256 char(64) DEFAULT NULL,
114116
provision_ts timestamp default current_timestamp"
115117

116118
# Check if the table exists
@@ -156,6 +158,24 @@ metadata_gather() {
156158
SIGNED_BOOT_VALUE="0"
157159
fi
158160

161+
# Determine OS image metadata if available
162+
OS_IMAGE_FILENAME=""
163+
OS_IMAGE_SHA256=""
164+
if [ -n "${GOLD_MASTER_OS_FILE}" ] && [ -f "${GOLD_MASTER_OS_FILE}" ]; then
165+
OS_IMAGE_FILENAME="$(basename -- "${GOLD_MASTER_OS_FILE}")"
166+
# Prefer sidecar file written by UI backend: <image>.sha256 (first token/line)
167+
if [ -f "${GOLD_MASTER_OS_FILE}.sha256" ]; then
168+
# Read first non-empty token from sidecar
169+
OS_IMAGE_SHA256="$(head -n1 "${GOLD_MASTER_OS_FILE}.sha256" | awk '{print $1}')"
170+
fi
171+
# Fallback to calculating if sidecar missing or empty
172+
if [ -z "${OS_IMAGE_SHA256}" ] && command -v sha256sum >/dev/null 2>&1; then
173+
OS_IMAGE_SHA256="$(sha256sum "${GOLD_MASTER_OS_FILE}" | awk '{print $1}')"
174+
# Best-effort: persist to sidecar for reuse by UI and future runs
175+
printf '%s\n' "${OS_IMAGE_SHA256}" > "${GOLD_MASTER_OS_FILE}.sha256" 2>/dev/null || true
176+
fi
177+
fi
178+
159179
# Insert new device data
160180
sqlite3 "${RPI_SB_PROVISIONER_MANUFACTURING_DB}" \
161181
"INSERT INTO devices( \
@@ -175,7 +195,9 @@ metadata_gather() {
175195
jtag_locked, \
176196
eeprom_write_protected, \
177197
pubkey_programmed, \
178-
signed_boot_enabled \
198+
signed_boot_enabled, \
199+
os_image_filename, \
200+
os_image_sha256 \
179201
) VALUES ( \
180202
'${BOARD_STR}', \
181203
'${TARGET_DEVICE_SERIAL}', \
@@ -193,7 +215,9 @@ metadata_gather() {
193215
${JTAG_LOCKED_VALUE}, \
194216
${EEPROM_WP_VALUE}, \
195217
${PUBKEY_PROGRAMMED_VALUE}, \
196-
${SIGNED_BOOT_VALUE} \
218+
${SIGNED_BOOT_VALUE}, \
219+
'${OS_IMAGE_FILENAME}', \
220+
'${OS_IMAGE_SHA256}' \
197221
);" > /dev/null 2>&1
198222
announce_stop "Manufacturing Database Insertion"
199223
fi

provisioner-service/src/images.cpp

Lines changed: 109 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -467,31 +467,77 @@ namespace provisioner {
467467
}
468468
sha256Cache.insert_or_assign(imageName, result);
469469

470+
// Write/refresh sidecar file with SHA256 to optimize future reads
471+
try {
472+
std::filesystem::path sidecarPath(IMAGES_PATH);
473+
sidecarPath /= imageName;
474+
sidecarPath += ".sha256";
475+
std::ofstream sidecarFile(sidecarPath, std::ios::out | std::ios::trunc);
476+
if (sidecarFile.is_open()) {
477+
sidecarFile << sha256 << "\n";
478+
sidecarFile.close();
479+
AuditLog::logFileSystemAccess("WRITE", sidecarPath.string(), true, "",
480+
std::string("Wrote SHA256 sidecar for: ") + imageName);
481+
} else {
482+
AuditLog::logFileSystemAccess("WRITE", sidecarPath.string(), false, "",
483+
std::string("Failed to open SHA256 sidecar for writing: ") + imageName);
484+
}
485+
} catch (const std::filesystem::filesystem_error& e) {
486+
LOG_ERROR << "Failed to write SHA256 sidecar (filesystem) for " << imageName << ": " << e.what();
487+
} catch (const std::ios_base::failure& e) {
488+
LOG_ERROR << "Failed to write SHA256 sidecar (io) for " << imageName << ": " << e.what();
489+
} catch (const std::exception& e) {
490+
LOG_ERROR << "Failed to write SHA256 sidecar for " << imageName << ": " << e.what();
491+
}
492+
470493
// Log successful SHA256 calculation
471494
AuditLog::logFileSystemAccess("SHA256_COMPLETE", imagePath.string(), true, "",
472495
"SHA256 calculated for: " + imageName + " = " + sha256);
473496

474497
// Broadcast completion to WebSocket clients
475498
SHA256WebSocketController::broadcastUpdate(imageName, result);
476499
}
500+
} catch (const std::filesystem::filesystem_error& e) {
501+
// Update cache with more specific filesystem error
502+
std::lock_guard<std::mutex> lock(sha256Cache_mutex);
503+
SHA256Result result(std::string("Filesystem error: ") + e.what(), SHA256Status::ERROR, false);
504+
auto it = sha256Cache.find(imageName);
505+
if (it != sha256Cache.end()) {
506+
result.cancellation_token = it->second.cancellation_token;
507+
}
508+
sha256Cache.insert_or_assign(imageName, result);
509+
std::filesystem::path imagePath(IMAGES_PATH);
510+
imagePath /= imageName;
511+
AuditLog::logFileSystemAccess("SHA256_ERROR", imagePath.string(), false, "",
512+
"SHA256 calculation filesystem error for: " + imageName + " - " + e.what());
513+
SHA256WebSocketController::broadcastUpdate(imageName, result);
514+
} catch (const std::bad_alloc& e) {
515+
// Update cache with memory error
516+
std::lock_guard<std::mutex> lock(sha256Cache_mutex);
517+
SHA256Result result(std::string("Memory error: ") + e.what(), SHA256Status::ERROR, false);
518+
auto it = sha256Cache.find(imageName);
519+
if (it != sha256Cache.end()) {
520+
result.cancellation_token = it->second.cancellation_token;
521+
}
522+
sha256Cache.insert_or_assign(imageName, result);
523+
std::filesystem::path imagePath(IMAGES_PATH);
524+
imagePath /= imageName;
525+
AuditLog::logFileSystemAccess("SHA256_ERROR", imagePath.string(), false, "",
526+
"SHA256 calculation memory error for: " + imageName + " - " + e.what());
527+
SHA256WebSocketController::broadcastUpdate(imageName, result);
477528
} catch (const std::exception& e) {
478-
// Update cache with error
529+
// General error fallback
479530
std::lock_guard<std::mutex> lock(sha256Cache_mutex);
480531
SHA256Result result(std::string("Error: ") + e.what(), SHA256Status::ERROR, false);
481-
// Preserve cancellation token from existing entry if it exists
482532
auto it = sha256Cache.find(imageName);
483533
if (it != sha256Cache.end()) {
484534
result.cancellation_token = it->second.cancellation_token;
485535
}
486536
sha256Cache.insert_or_assign(imageName, result);
487-
488-
// Log SHA256 calculation error
489537
std::filesystem::path imagePath(IMAGES_PATH);
490538
imagePath /= imageName;
491539
AuditLog::logFileSystemAccess("SHA256_ERROR", imagePath.string(), false, "",
492540
"SHA256 calculation error for: " + imageName + " - " + e.what());
493-
494-
// Broadcast error to WebSocket clients
495541
SHA256WebSocketController::broadcastUpdate(imageName, result);
496542
}
497543
}
@@ -551,6 +597,8 @@ namespace provisioner {
551597
// Cap at reasonable maximum (120 minutes) for very large files
552598
return std::min(estimatedMinutes, 120U);
553599
}
600+
} catch (const std::filesystem::filesystem_error& e) {
601+
LOG_ERROR << "Filesystem error calculating timeout for file " << filename << ": " << e.what();
554602
} catch (const std::exception& e) {
555603
LOG_ERROR << "Error calculating timeout for file " << filename << ": " << e.what();
556604
}
@@ -665,6 +713,8 @@ namespace provisioner {
665713
requestSHA256Calculation(imageName);
666714
}
667715
}
716+
} catch (const std::filesystem::filesystem_error& e) {
717+
LOG_ERROR << "Filesystem error scanning images directory for automatic SHA256 calculation: " << e.what();
668718
} catch (const std::exception& e) {
669719
LOG_ERROR << "Error scanning images directory for automatic SHA256 calculation: " << e.what();
670720
}
@@ -876,6 +926,18 @@ namespace provisioner {
876926
unsigned int estimatedMinutes = static_cast<unsigned int>(std::ceil(estimatedSeconds / 60.0));
877927
result["estimated_process_minutes"] = static_cast<Json::UInt>(estimatedMinutes);
878928

929+
} catch (const std::filesystem::filesystem_error& e) {
930+
LOG_ERROR << "Error getting metadata for " << imageName << ": " << e.what();
931+
auto resp = provisioner::utils::createErrorResponse(
932+
req,
933+
"Error retrieving image metadata",
934+
drogon::k500InternalServerError,
935+
"Metadata Error",
936+
"METADATA_ERROR",
937+
e.what()
938+
);
939+
callback(resp);
940+
return;
879941
} catch (const std::exception& e) {
880942
LOG_ERROR << "Error getting metadata for " << imageName << ": " << e.what();
881943
auto resp = provisioner::utils::createErrorResponse(
@@ -910,21 +972,47 @@ namespace provisioner {
910972
ImageInfo info;
911973
info.name = imagePath.filename().string();
912974

913-
// Get SHA256 from cache if available
914-
{
915-
std::lock_guard<std::mutex> lock(sha256Cache_mutex);
916-
auto it = sha256Cache.find(info.name);
917-
if (it != sha256Cache.end()) {
918-
if (it->second.status == SHA256Status::COMPLETE) {
919-
info.sha256 = it->second.value;
920-
} else if (it->second.status == SHA256Status::PENDING) {
921-
info.sha256 = "Calculating...";
975+
// Prefer sidecar file if present; fall back to cache status
976+
try {
977+
std::filesystem::path sidecarPath = imagePath;
978+
sidecarPath += ".sha256";
979+
if (std::filesystem::exists(sidecarPath)) {
980+
std::ifstream in(sidecarPath);
981+
std::string line;
982+
if (in && std::getline(in, line)) {
983+
// Trim trailing whitespace
984+
while (!line.empty() && (line.back()==' ' || line.back()=='\t' || line.back()=='\n' || line.back()=='\r')) {
985+
line.pop_back();
986+
}
987+
info.sha256 = line;
988+
}
989+
}
990+
} catch (const std::filesystem::filesystem_error& e) {
991+
// Ignore filesystem errors while reading sidecar, but log at debug level
992+
LOG_DEBUG << "Ignoring sidecar filesystem read error for " << imagePath.filename().string() << ": " << e.what();
993+
} catch (const std::ios_base::failure& e) {
994+
LOG_DEBUG << "Ignoring sidecar IO error for " << imagePath.filename().string() << ": " << e.what();
995+
} catch (const std::exception& e) {
996+
LOG_DEBUG << "Ignoring sidecar generic read error for " << imagePath.filename().string() << ": " << e.what();
997+
}
998+
999+
if (info.sha256.empty()) {
1000+
// Get SHA256 from cache if available
1001+
{
1002+
std::lock_guard<std::mutex> lock(sha256Cache_mutex);
1003+
auto it = sha256Cache.find(info.name);
1004+
if (it != sha256Cache.end()) {
1005+
if (it->second.status == SHA256Status::COMPLETE) {
1006+
info.sha256 = it->second.value;
1007+
} else if (it->second.status == SHA256Status::PENDING) {
1008+
info.sha256 = "Calculating...";
1009+
} else {
1010+
info.sha256 = "Error";
1011+
}
9221012
} else {
923-
info.sha256 = "Error";
1013+
// Not in cache yet, calculation may still be pending
1014+
info.sha256 = "Calculating...";
9241015
}
925-
} else {
926-
// Not in cache yet, calculation may still be pending
927-
info.sha256 = "Calculating...";
9281016
}
9291017
}
9301018

@@ -1190,6 +1278,8 @@ namespace provisioner {
11901278

11911279
result["sha256"] = "Calculating..."; // SHA256 calculation in progress
11921280
resp->setBody(result.toStyledString());
1281+
} catch (const std::filesystem::filesystem_error& e) {
1282+
LOG_ERROR << "Failed to save uploaded file (filesystem): " << e.what();
11931283
} catch (const std::exception& e) {
11941284
LOG_ERROR << "Failed to save uploaded file: " << e.what();
11951285

provisioner-service/src/views/manufacturing.csp

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,8 @@
289289
<th>EEPROM WP</th>
290290
<th>Pubkey Prog.</th>
291291
<th>Signed Boot</th>
292+
<th>OS Image Filename</th>
293+
<th>OS Image SHA256</th>
292294
</tr>
293295
</thead>
294296
<tbody>
@@ -317,13 +319,15 @@
317319
<td class="security-field">${formatSecurityField(device.eeprom_write_protected)}</td>
318320
<td class="security-field">${formatSecurityField(device.pubkey_programmed)}</td>
319321
<td class="security-field">${formatSecurityField(device.signed_boot_enabled)}</td>
322+
<td>${device.os_image_filename || ''}</td>
323+
<td style="font-family: monospace;">${device.os_image_sha256 || ''}</td>
320324
</tr>
321325
`;
322326
});
323327
} else {
324328
tableHtml += `
325329
<tr>
326-
<td colspan="19" class="no-devices">No manufacturing data available</td>
330+
<td colspan="21" class="no-devices">No manufacturing data available</td>
327331
</tr>
328332
`;
329333
}

0 commit comments

Comments
 (0)