Skip to content

Commit a36f483

Browse files
committed
IDP: Validate provisioning map against schema
Introduce schema validation of the Provisioning Map (PMAP). The schema is selected by a new base image layer config variable, with default schemas residing under layer/base/schemas/provisionmap/. The generic post-image hook now validates the PMAP automatically on every build. This ensures the staged JSON is valid for the provisioning side to process. Drop (now redundant) checks on some JSON objects in the pmap helper. Schema validation is performed on every invocation. Clarify some description wording in the AB layer. Fixup bad partition ref in the AB layer clear pmap for A.system. Update docs.
1 parent f8388b7 commit a36f483

File tree

10 files changed

+270
-79
lines changed

10 files changed

+270
-79
lines changed

bin/pmap

Lines changed: 65 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import json
77
import sys
88
import uuid
99
import re
10+
import os
1011

1112

1213
VALID_ROLES = ["boot", "system"]
@@ -41,55 +42,59 @@ def pmap_version(data):
4142
sys.exit(1)
4243

4344

44-
# Top level PMAP validator
45-
def validate(data):
46-
major, minor, patch = pmap_version(data)
47-
# TODO
48-
return major, minor, patch
49-
50-
51-
# Validates a static object and returns mandatory keys
52-
def chk_static(data):
53-
role = data.get("role")
54-
55-
# role: (mandatory, string)
56-
if not role:
57-
sys.stderr.write("Error: role is mandatory in a static object.\n")
58-
sys.exit(1)
45+
def _load_validator(schema_path):
46+
try:
47+
from jsonschema import Draft7Validator
48+
except ImportError:
49+
sys.stderr.write("Error: jsonschema not installed.\n")
50+
sys.exit(2)
5951

60-
if role not in VALID_ROLES:
61-
sys.stderr.write(f"Error: Invalid 'role': '{role}'. Must be one of {VALID_ROLES}.\n")
52+
try:
53+
with open(schema_path, "r", encoding="utf-8") as f:
54+
schema = json.load(f)
55+
Draft7Validator.check_schema(schema)
56+
return Draft7Validator(schema)
57+
except Exception as e:
58+
sys.stderr.write(f"Error: failed to load schema '{schema_path}': {e}\n")
6259
sys.exit(1)
6360

64-
# id: (optional, string)
65-
if "id" in data:
66-
id_val = data.get("id")
67-
if not isinstance(id_val, str):
68-
sys.stderr.write("Error: id is not a string.\n")
69-
sys.exit(1)
7061

71-
# uuid: (optional, valid UUID string); allow placeholders like <FOO>
72-
if "uuid" in data:
73-
uuid_val = data.get("uuid")
74-
if not isinstance(uuid_val, str):
75-
sys.stderr.write("Error: uuid is not a string.\n")
76-
sys.exit(1)
77-
# Skip strict validation for obvious placeholders
78-
if "<" in uuid_val or ">" in uuid_val:
79-
pass
62+
# Top level PMAP validator (returns parsed version on success)
63+
def validate(data, schema_path=None):
64+
# Resolve schema path: explicit > alongside script > skip schema
65+
validator = None
66+
if schema_path is None:
67+
# Try default schema next to this script
68+
default_schema = os.path.join(os.path.dirname(os.path.abspath(__file__)),
69+
"provisionmap.schema.json")
70+
if os.path.isfile(default_schema):
71+
schema_path = default_schema
72+
if schema_path:
73+
validator = _load_validator(schema_path)
74+
75+
if validator is not None:
76+
# Always validate only the provisionmap subtree
77+
if isinstance(data, list):
78+
pmap = data
8079
else:
81-
try:
82-
uuid.UUID(uuid_val)
83-
except ValueError:
84-
if (re.match(r'^[0-9a-f]{8}$', uuid_val, re.IGNORECASE) or
85-
re.match(r'^[0-9a-f]{4}-[0-9a-f]{4}$', uuid_val, re.IGNORECASE)):
86-
pass # Accept as valid VFAT UUID (label)
80+
pmap = get_key(data, "layout.provisionmap")
81+
if pmap is None:
82+
sys.stderr.write("Error: layout.provisionmap not found in JSON.\n")
83+
sys.exit(1)
84+
doc = {"layout": {"provisionmap": pmap}}
85+
86+
errors = sorted(validator.iter_errors(doc), key=lambda e: list(e.path))
87+
if errors:
88+
sys.stderr.write(f"Error: provisionmap schema validation failed ({len(errors)} errors)\n")
89+
for e in errors:
90+
path = "/".join(str(p) for p in e.path)
91+
if path:
92+
sys.stderr.write(f" at $.{path}: {e.message}\n")
8793
else:
88-
sys.stderr.write(f"Error: uuid is invalid: '{uuid_val}'.\n")
89-
sys.exit(1)
94+
sys.stderr.write(f" at $: {e.message}\n")
95+
sys.exit(1)
9096

91-
# Return mandatory
92-
return role
97+
return pmap_version(data)
9398

9499

95100
"""
@@ -144,7 +149,7 @@ def slotvars(data):
144149
static = part.get("static")
145150
if static is None:
146151
continue
147-
role = chk_static(static)
152+
role = static["role"]
148153
idx = next_mapper_index(mname)
149154
triplets[(slot, role)] = f"mapper:{mname}:{idx}"
150155
continue
@@ -164,7 +169,7 @@ def slotvars(data):
164169
static = part.get("static")
165170
if static is None:
166171
continue
167-
role = chk_static(static)
172+
role = static["role"]
168173
idx = next_mapper_index(mname)
169174
triplets[(slot, role)] = f"mapper:{mname}:{idx}"
170175

@@ -175,7 +180,7 @@ def slotvars(data):
175180
static = part.get("static")
176181
if static is None:
177182
continue
178-
role = chk_static(static)
183+
role = static["role"]
179184
triplets[(slot, role)] = f"::{physical_part_index}"
180185

181186
continue
@@ -223,25 +228,32 @@ def get_key(data, key_path, default=None):
223228

224229
if __name__ == '__main__':
225230
parser = argparse.ArgumentParser(
226-
description='PMAP helper')
231+
description='IDP Map File Utility')
227232

228233
parser.add_argument("-f", "--file",
229-
help="Path to PMAP file",
234+
help="Path to Provisioning Map (PMAP) file",
230235
required=True)
231236

237+
parser.add_argument("--schema",
238+
help="Path to JSON schema")
239+
232240
parser.add_argument("-s", "--slotvars",
233241
action="store_true",
234-
help="Print slot.map triplets (a.boot=..., a.system=..., b.boot=..., b.system=...)")
242+
help="Print slot.map triplets")
235243

236244
parser.add_argument("--get-key",
237245
help="Dot-separated key path to retrieve from PMAP JSON")
238246

239247
args = parser.parse_args()
240248

241-
with open(args.file) as f:
242-
data = json.load(f)
249+
try:
250+
with open(args.file) as f:
251+
data = json.load(f)
252+
except Exception as e:
253+
sys.stderr.write(f"Error: invalid JSON: {e}\n")
254+
sys.exit(1)
243255

244-
major, minor, patch = validate(data)
256+
major, minor, patch = validate(data, args.schema)
245257

246258
if args.get_key:
247259
value = get_key(data, args.get_key)
@@ -251,8 +263,7 @@ if __name__ == '__main__':
251263
print(value)
252264
sys.exit(0)
253265

254-
major, minor, patch = validate(data)
255-
256266
if args.slotvars:
257-
slotvars(data)
267+
pmap = data if isinstance(data, list) else get_key(data, "layout.provisionmap")
268+
slotvars(pmap)
258269
sys.exit(0);

depends

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ uuidgen:uuid-runtime
2121
fdisk
2222
python3-yaml
2323
python3-debian
24+
python3-jsonschema
2425
# doc gen only
2526
# python3-markdown
2627
# asciidoctor

docs/layer/image-base.html

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
<div class="header">
124124
<h1>image-base</h1>
125125
<span class="badge">image</span>
126-
<span class="badge">v1.0.0</span>
126+
<span class="badge">v1.1.0</span>
127127
<p>Default image settings and build attributes.</p>
128128
</div>
129129

@@ -314,10 +314,10 @@ <h2>Configuration Variables</h2>
314314
<tr>
315315
<td><code>IGconf_image_pmap</code></td>
316316
<td>Set the identifier string for the image Provisioning
317-
Map. The Provisioning Map defines how the image will be provisioned on the
318-
device for which it's intended. The pmap is an extension of the Image
319-
Description JSON file generated by the build. Providing a pmap is optional,
320-
but it is mandatory for provisioning the image using Raspberry Pi tools.</td>
317+
Map (PMAP). The PMAP file defines how the image will be provisioned on the
318+
device for which it's intended. The PMAP is part of the Image Description
319+
JSON file generated by the build. Providing a PMAP is optional, but is
320+
mandatory for provisioning the image using Raspberry Pi tools.</td>
321321
<td>
322322

323323
<code>&lt;empty&gt;</code>
@@ -329,6 +329,22 @@ <h2>Configuration Variables</h2>
329329
</td>
330330
</tr>
331331

332+
<tr>
333+
<td><code>IGconf_image_pmap_schema</code></td>
334+
<td>Image Description PMAP schema for validation</td>
335+
<td>
336+
337+
338+
<code class="long-default">${DIRECTORY}/schemas/provisionmap/v1/schema.json</code>
339+
340+
341+
</td>
342+
<td>Non-empty string value</td>
343+
<td>
344+
<a href="variable-validation.html#set-policies" class="badge policy-lazy" title="Click for policy and validation help">lazy</a>
345+
</td>
346+
</tr>
347+
332348
<tr>
333349
<td><code>IGconf_image_outputdir</code></td>
334350
<td>Location of all image build artefacts.</td>

docs/layer/image-rota.html

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@
123123
<div class="header">
124124
<h1>image-rota</h1>
125125
<span class="badge">image</span>
126-
<span class="badge">v4.0.0</span>
126+
<span class="badge">v4.1.0</span>
127127
<p>Immutable GPT A/B layout for rotational OTA updates,
128128
boot/system redundancy, and a shared persistent data partition.</p>
129129
</div>
@@ -488,17 +488,17 @@ <h2>Configuration Variables</h2>
488488
<tr>
489489
<td><code>IGconf_image_pmap</code></td>
490490
<td>Provisioning Map type for this image layout.
491-
All partitions will be provisioned unencrypted (clear).
492-
System partitions will be provisioned encrypted (crypt).
493-
System B will be provisioned encrypted (hybrid). Development only.</td>
491+
clear: All partitions will be provisioned unencrypted.
492+
crypt: All partitions except <slot>:boot will be provisioned encrypted.
493+
hybrid: B:system will be provisioned encrypted (development only).</td>
494494
<td>
495495

496496

497497
<code>clear</code>
498498

499499

500500
</td>
501-
<td>Must be one of: clear, hybrid</td>
501+
<td>Must be one of: clear, crypt, hybrid</td>
502502
<td>
503503
<a href="variable-validation.html#set-policies" class="badge policy-immediate" title="Click for policy and validation help">immediate</a>
504504
</td>

image/gpt/ab_userdata/bdebstrap/customize05-rootfs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ sed -i \
3030
-e "s|<CRYPT_UUID>|$CRYPT_UUID|g" ${IGconf_image_outputdir}/provisionmap.json
3131

3232

33-
# Generate slot map. IDP currently doesn't preserve GPT labels so
34-
# add the map as a fallback so a provisioned image boots.
35-
pmap -f "${IGconf_image_outputdir}/provisionmap.json" -s > "$1/boot/slot.map"
33+
# Generate slot map. IDP does preserve GPT labels but this has yet make it to
34+
# mainline. Add the map as a fallback so a provisioned image boots.
35+
pmap --schema "$IGconf_image_pmap_schema" \
36+
--file "${IGconf_image_outputdir}/provisionmap.json" \
37+
--slotvars > "$1/boot/slot.map"
3638

3739

3840
# Hint to initramfs-tools we have an ext4 rootfs

image/gpt/ab_userdata/device/provisionmap-clear.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
"A": {
4848
"partitions": [
4949
{
50-
"image": "system_b",
50+
"image": "system_a",
5151
"static": {
5252
"uuid": "<SYSTEM_UUID>",
5353
"role": "system"

image/gpt/ab_userdata/image.yaml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
# X-Env-Layer-Category: image
44
# X-Env-Layer-Desc: Immutable GPT A/B layout for rotational OTA updates,
55
# boot/system redundancy, and a shared persistent data partition.
6-
# X-Env-Layer-Version: 4.0.0
6+
# X-Env-Layer-Version: 4.1.0
77
# X-Env-Layer-Requires: image-base,device-base,rpi-ab-slot-mapper,systemd-min
88
# X-Env-Layer-Provides: image
99
#
@@ -38,11 +38,11 @@
3838
#
3939
# X-Env-Var-pmap: clear
4040
# X-Env-Var-pmap-Desc: Provisioning Map type for this image layout.
41-
# All partitions will be provisioned unencrypted (clear).
42-
# System partitions will be provisioned encrypted (crypt).
43-
# System B will be provisioned encrypted (hybrid). Development only.
41+
# clear: All partitions will be provisioned unencrypted.
42+
# crypt: All partitions except <slot>:boot will be provisioned encrypted.
43+
# hybrid: B:system will be provisioned encrypted (development only).
4444
# X-Env-Var-pmap-Required: n
45-
# X-Env-Var-pmap-Valid: clear,hybrid
45+
# X-Env-Var-pmap-Valid: clear,crypt,hybrid
4646
# X-Env-Var-pmap-Set: y
4747
#
4848
# X-Env-Var-ptable_protect: $( [ "${IGconf_device_storage_type:-}" = "emmc" ] && echo y || echo n )

image/post-image.sh

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,12 @@ if [ -f ${1}/genimage.cfg ] ; then
1414
done
1515

1616
pmap="${IGconf_image_outputdir}/provisionmap.json"
17-
[[ -f "$pmap" ]] && opts+=('-m' "$pmap")
17+
if [ -f "$pmap" ] ; then
18+
# Validate pmap against the schema
19+
pmap --schema "$IGconf_image_pmap_schema" --file "$pmap" ||
20+
die "Installed Provisioning Map failed to validate."
21+
opts+=('-m' "$pmap")
22+
fi
1823

1924
# Generate description for IDP
2025
image2json -g ${1}/genimage.cfg "${opts[@]}" > ${1}/image.json

layer/base/image-base.yaml

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# X-Env-Layer-Name: image-base
33
# X-Env-Layer-Category: image
44
# X-Env-Layer-Desc: Default image settings and build attributes.
5-
# X-Env-Layer-Version: 1.0.0
5+
# X-Env-Layer-Version: 1.1.0
66
# X-Env-Layer-Requires: sys-build-base,sbom-base,target-config,artefact-base,deploy-base
77
# X-Env-Layer-RequiresProvider: device
88
#
@@ -39,14 +39,20 @@
3939
#
4040
# X-Env-Var-pmap:
4141
# X-Env-Var-pmap-Desc: Set the identifier string for the image Provisioning
42-
# Map. The Provisioning Map defines how the image will be provisioned on the
43-
# device for which it's intended. The pmap is an extension of the Image
44-
# Description JSON file generated by the build. Providing a pmap is optional,
45-
# but it is mandatory for provisioning the image using Raspberry Pi tools.
42+
# Map (PMAP). The PMAP file defines how the image will be provisioned on the
43+
# device for which it's intended. The PMAP is part of the Image Description
44+
# JSON file generated by the build. Providing a PMAP is optional, but is
45+
# mandatory for provisioning the image using Raspberry Pi tools.
4646
# X-Env-Var-pmap-Required: n
4747
# X-Env-Var-pmap-Valid: string-or-empty
4848
# X-Env-Var-pmap-Set: lazy
4949
#
50+
# X-Env-Var-pmap_schema: ${DIRECTORY}/schemas/provisionmap/v1/schema.json
51+
# X-Env-Var-pmap_schema-Desc: Image Description PMAP schema for validation
52+
# X-Env-Var-pmap_schema-Required: n
53+
# X-Env-Var-pmap_schema-Valid: string
54+
# X-Env-Var-pmap_schema-Set: lazy
55+
#
5056
# X-Env-Var-outputdir: ${IGconf_sys_workroot}/image-${IGconf_image_name}
5157
# X-Env-Var-outputdir-Desc: Location of all image build artefacts.
5258
# X-Env-Var-outputdir-Required: n

0 commit comments

Comments
 (0)