Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 65 additions & 54 deletions bin/pmap
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import json
import sys
import uuid
import re
import os


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


# Top level PMAP validator
def validate(data):
major, minor, patch = pmap_version(data)
# TODO
return major, minor, patch


# Validates a static object and returns mandatory keys
def chk_static(data):
role = data.get("role")

# role: (mandatory, string)
if not role:
sys.stderr.write("Error: role is mandatory in a static object.\n")
sys.exit(1)
def _load_validator(schema_path):
try:
from jsonschema import Draft7Validator
except ImportError:
sys.stderr.write("Error: jsonschema not installed.\n")
sys.exit(2)

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

# id: (optional, string)
if "id" in data:
id_val = data.get("id")
if not isinstance(id_val, str):
sys.stderr.write("Error: id is not a string.\n")
sys.exit(1)

# uuid: (optional, valid UUID string); allow placeholders like <FOO>
if "uuid" in data:
uuid_val = data.get("uuid")
if not isinstance(uuid_val, str):
sys.stderr.write("Error: uuid is not a string.\n")
sys.exit(1)
# Skip strict validation for obvious placeholders
if "<" in uuid_val or ">" in uuid_val:
pass
# Top level PMAP validator (returns parsed version on success)
def validate(data, schema_path=None):
# Resolve schema path: explicit > alongside script > skip schema
validator = None
if schema_path is None:
# Try default schema next to this script
default_schema = os.path.join(os.path.dirname(os.path.abspath(__file__)),
"provisionmap.schema.json")
if os.path.isfile(default_schema):
schema_path = default_schema
if schema_path:
validator = _load_validator(schema_path)

if validator is not None:
# Always validate only the provisionmap subtree
if isinstance(data, list):
pmap = data
else:
try:
uuid.UUID(uuid_val)
except ValueError:
if (re.match(r'^[0-9a-f]{8}$', uuid_val, re.IGNORECASE) or
re.match(r'^[0-9a-f]{4}-[0-9a-f]{4}$', uuid_val, re.IGNORECASE)):
pass # Accept as valid VFAT UUID (label)
pmap = get_key(data, "layout.provisionmap")
if pmap is None:
sys.stderr.write("Error: layout.provisionmap not found in JSON.\n")
sys.exit(1)
doc = {"layout": {"provisionmap": pmap}}

errors = sorted(validator.iter_errors(doc), key=lambda e: list(e.path))
if errors:
sys.stderr.write(f"Error: provisionmap schema validation failed ({len(errors)} errors)\n")
for e in errors:
path = "/".join(str(p) for p in e.path)
if path:
sys.stderr.write(f" at $.{path}: {e.message}\n")
else:
sys.stderr.write(f"Error: uuid is invalid: '{uuid_val}'.\n")
sys.exit(1)
sys.stderr.write(f" at $: {e.message}\n")
sys.exit(1)

# Return mandatory
return role
return pmap_version(data)


"""
Expand Down Expand Up @@ -144,7 +149,7 @@ def slotvars(data):
static = part.get("static")
if static is None:
continue
role = chk_static(static)
role = static["role"]
idx = next_mapper_index(mname)
triplets[(slot, role)] = f"mapper:{mname}:{idx}"
continue
Expand All @@ -164,7 +169,7 @@ def slotvars(data):
static = part.get("static")
if static is None:
continue
role = chk_static(static)
role = static["role"]
idx = next_mapper_index(mname)
triplets[(slot, role)] = f"mapper:{mname}:{idx}"

Expand All @@ -175,7 +180,7 @@ def slotvars(data):
static = part.get("static")
if static is None:
continue
role = chk_static(static)
role = static["role"]
triplets[(slot, role)] = f"::{physical_part_index}"

continue
Expand Down Expand Up @@ -223,25 +228,32 @@ def get_key(data, key_path, default=None):

if __name__ == '__main__':
parser = argparse.ArgumentParser(
description='PMAP helper')
description='IDP Map File Utility')

parser.add_argument("-f", "--file",
help="Path to PMAP file",
help="Path to Provisioning Map (PMAP) file",
required=True)

parser.add_argument("--schema",
help="Path to JSON schema")

parser.add_argument("-s", "--slotvars",
action="store_true",
help="Print slot.map triplets (a.boot=..., a.system=..., b.boot=..., b.system=...)")
help="Print slot.map triplets")

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

args = parser.parse_args()

with open(args.file) as f:
data = json.load(f)
try:
with open(args.file) as f:
data = json.load(f)
except Exception as e:
sys.stderr.write(f"Error: invalid JSON: {e}\n")
sys.exit(1)

major, minor, patch = validate(data)
major, minor, patch = validate(data, args.schema)

if args.get_key:
value = get_key(data, args.get_key)
Expand All @@ -251,8 +263,7 @@ if __name__ == '__main__':
print(value)
sys.exit(0)

major, minor, patch = validate(data)

if args.slotvars:
slotvars(data)
pmap = data if isinstance(data, list) else get_key(data, "layout.provisionmap")
slotvars(pmap)
sys.exit(0);
1 change: 1 addition & 0 deletions depends
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ uuidgen:uuid-runtime
fdisk
python3-yaml
python3-debian
python3-jsonschema
# doc gen only
# python3-markdown
# asciidoctor
Expand Down
26 changes: 21 additions & 5 deletions docs/layer/image-base.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
<div class="header">
<h1>image-base</h1>
<span class="badge">image</span>
<span class="badge">v1.0.0</span>
<span class="badge">v1.1.0</span>
<p>Default image settings and build attributes.</p>
</div>

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

<code>&lt;empty&gt;</code>
Expand All @@ -329,6 +329,22 @@ <h2>Configuration Variables</h2>
</td>
</tr>

<tr>
<td><code>IGconf_image_pmap_schema</code></td>
<td>Image Description PMAP schema for validation</td>
<td>


<code class="long-default">${DIRECTORY}/schemas/provisionmap/v1/schema.json</code>


</td>
<td>Non-empty string value</td>
<td>
<a href="variable-validation.html#set-policies" class="badge policy-lazy" title="Click for policy and validation help">lazy</a>
</td>
</tr>

<tr>
<td><code>IGconf_image_outputdir</code></td>
<td>Location of all image build artefacts.</td>
Expand Down
14 changes: 8 additions & 6 deletions docs/layer/image-rota.html
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
<div class="header">
<h1>image-rota</h1>
<span class="badge">image</span>
<span class="badge">v4.0.0</span>
<span class="badge">v4.1.0</span>
<p>Immutable GPT A/B layout for rotational OTA updates,
boot/system redundancy, and a shared persistent data partition.</p>
</div>
Expand Down Expand Up @@ -454,7 +454,7 @@ <h2>Configuration Variables</h2>

<tr>
<td><code>IGconf_image_data_part_size</code></td>
<td>Writable storage partition retained across
<td>Writable data partition retained across
slot rotations.</td>
<td>

Expand Down Expand Up @@ -488,17 +488,19 @@ <h2>Configuration Variables</h2>
<tr>
<td><code>IGconf_image_pmap</code></td>
<td>Provisioning Map type for this image layout.
All partitions will be provisioned unencrypted (clear).
System partitions will be provisioned encrypted (crypt).
System B will be provisioned encrypted (hybrid). Development only.</td>
clear: All partitions will be provisioned unencrypted.
crypt: All non-boot partitions will be provisioned encrypted.
cryptslots: Only system OS partitions will be provisioned encrypted.
cryptdata: Only the data partition will be provisioned encrypted.
crypthybrid: B:system OS partition will be provisioned encrypted (dev only).</td>
<td>


<code>clear</code>


</td>
<td>Must be one of: clear, hybrid</td>
<td>Must be one of: clear, crypt, cryptslots, cryptdata, crypthybrid</td>
<td>
<a href="variable-validation.html#set-policies" class="badge policy-immediate" title="Click for policy and validation help">immediate</a>
</td>
Expand Down
8 changes: 5 additions & 3 deletions image/gpt/ab_userdata/bdebstrap/customize05-rootfs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,11 @@ sed -i \
-e "s|<CRYPT_UUID>|$CRYPT_UUID|g" ${IGconf_image_outputdir}/provisionmap.json


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


# Hint to initramfs-tools we have an ext4 rootfs
Expand Down
2 changes: 1 addition & 1 deletion image/gpt/ab_userdata/device/provisionmap-clear.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
"A": {
"partitions": [
{
"image": "system_b",
"image": "system_a",
"static": {
"uuid": "<SYSTEM_UUID>",
"role": "system"
Expand Down
2 changes: 1 addition & 1 deletion image/gpt/ab_userdata/device/provisionmap-crypt.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[
{
"attributes": {
"PMAPversion": "1.3.0",
"PMAPversion": "1.3.1",
"system_type": "slotted"
}
},
Expand Down
Loading