Skip to content

Commit 7738fc4

Browse files
authored
Merge pull request #122 from learmj/idp_dev
IDP: Validate provisioning map against schema
2 parents f8388b7 + 02ea8c6 commit 7738fc4

File tree

14 files changed

+460
-83
lines changed

14 files changed

+460
-83
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: 8 additions & 6 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>
@@ -454,7 +454,7 @@ <h2>Configuration Variables</h2>
454454

455455
<tr>
456456
<td><code>IGconf_image_data_part_size</code></td>
457-
<td>Writable storage partition retained across
457+
<td>Writable data partition retained across
458458
slot rotations.</td>
459459
<td>
460460

@@ -488,17 +488,19 @@ <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 non-boot partitions will be provisioned encrypted.
493+
cryptslots: Only system OS partitions will be provisioned encrypted.
494+
cryptdata: Only the data partition will be provisioned encrypted.
495+
crypthybrid: B:system OS partition will be provisioned encrypted (dev only).</td>
494496
<td>
495497

496498

497499
<code>clear</code>
498500

499501

500502
</td>
501-
<td>Must be one of: clear, hybrid</td>
503+
<td>Must be one of: clear, crypt, cryptslots, cryptdata, crypthybrid</td>
502504
<td>
503505
<a href="variable-validation.html#set-policies" class="badge policy-immediate" title="Click for policy and validation help">immediate</a>
504506
</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/device/provisionmap-crypt.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[
22
{
33
"attributes": {
4-
"PMAPversion": "1.3.0",
4+
"PMAPversion": "1.3.1",
55
"system_type": "slotted"
66
}
77
},

0 commit comments

Comments
 (0)