v1.2 to v2.0 Migration Guide
This guide covers migrating from productmd v1.2 to v2.0 format, both for compose producers (tools like pungi that generate metadata) and compose consumers (tools that read metadata).
Reading v2.0 Metadata (Consumers)
No code changes required
The productmd library automatically detects the format version and deserializes correctly. Existing code that reads v1.2 metadata will work with v2.0 files without modification:
from productmd.images import Images
# This works for both v1.2 and v2.0 files
images = Images()
images.load("images.json")
for variant in images.images:
for arch in images.images[variant]:
for image in images.images[variant][arch]:
print(image.path) # always available
print(image.checksums) # always available
The v1.2 compatibility fields (path, size, checksums) are
populated from the Location object during deserialization, so existing
code continues to work.
Accessing Location objects
To take advantage of v2.0 features (remote URLs, checksums, OCI
references), access the location property:
from productmd.images import Images
images = Images()
images.load("images.json")
for variant in images.images:
for arch in images.images[variant]:
for image in images.images[variant][arch]:
loc = image.location
print(loc.url) # "https://cdn.example.com/..."
print(loc.checksum) # "sha256:abc123..."
print(loc.size) # 2465792000
print(loc.local_path) # "Server/x86_64/iso/boot.iso"
print(loc.is_oci) # True if oci:// URL
print(loc.is_remote) # True if http/https/oci URL
For v1.2 files, image.location synthesizes a Location from the v1.2
fields (path, size, checksums) with no remote URL.
Iterating all locations
Use iter_all_locations() to iterate over every
artifact location across all metadata types:
from productmd.compose import Compose
from productmd.convert import iter_all_locations
compose = Compose("/path/to/compose")
for entry in iter_all_locations(
images=compose.images,
rpms=compose.rpms,
extra_files=compose.extra_files,
composeinfo=compose.info,
):
print(f"{entry.metadata_type}: {entry.path}")
if entry.location and entry.location.is_remote:
print(f" URL: {entry.location.url}")
Writing v2.0 Metadata (Producers)
Upgrading existing v1.2 composes
The simplest way to create v2.0 metadata is to upgrade existing v1.2 files using the CLI tool:
# Upgrade a compose directory
productmd upgrade \
--output /tmp/v2-metadata \
--base-url https://cdn.example.com/compose/ \
/mnt/compose
# Upgrade with checksum computation
productmd upgrade \
--output /tmp/v2-metadata \
--base-url https://cdn.example.com/compose/ \
--compute-checksums \
/mnt/compose
Or programmatically:
from productmd.compose import Compose
from productmd.convert import upgrade_to_v2
compose = Compose("/mnt/compose")
upgrade_to_v2(
output_dir="/tmp/v2-metadata",
base_url="https://cdn.example.com/compose/",
images=compose.images,
rpms=compose.rpms,
extra_files=compose.extra_files,
composeinfo=compose.info,
)
Creating v2.0 metadata from scratch
When generating new compose metadata, create Location objects directly:
from productmd.images import Images, Image
from productmd.location import Location
from productmd.version import VERSION_2_0
images = Images()
images.header.version = "2.0"
images.compose.id = "Fedora-41-20260204.0"
images.compose.type = "production"
images.compose.date = "20260204"
images.compose.respin = 0
images.output_version = VERSION_2_0
image = Image(images)
image.arch = "x86_64"
image.type = "dvd"
image.format = "iso"
image.subvariant = "Server"
image.disc_number = 1
image.disc_count = 1
image.mtime = 1738627200
image.volume_id = "Fedora-S-41-x86_64"
image.location = Location(
url="https://cdn.example.com/Server/x86_64/iso/boot.iso",
size=2465792000,
checksum="sha256:1a2b3c4d5e6f...",
local_path="Server/x86_64/iso/boot.iso",
)
images.add("Server", "x86_64", image)
images.dump("images.json")
Adding Locations to RPMs
from productmd.rpms import Rpms
from productmd.location import Location
from productmd.version import VERSION_2_0
rpms = Rpms()
rpms.compose.id = "Fedora-41-20260204.0"
rpms.compose.type = "production"
rpms.compose.date = "20260204"
rpms.compose.respin = 0
rpms.output_version = VERSION_2_0
rpms.add(
variant="Server",
arch="x86_64",
nevra="bash-0:5.2.26-3.fc41.x86_64",
path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm",
sigkey="a15b79cc",
category="binary",
srpm_nevra="bash-0:5.2.26-3.fc41.src",
location=Location(
url="https://cdn.example.com/Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm",
size=1849356,
checksum="sha256:6a7b8c9d...",
local_path="Server/x86_64/os/Packages/b/bash-5.2.26-3.fc41.x86_64.rpm",
),
)
Custom URL mapping
For composes where different artifact types are stored on different servers, use a URL mapper:
from productmd.convert import upgrade_to_v2
def my_url_mapper(local_path, variant, arch, metadata_type):
if metadata_type == "image":
return f"https://images.example.com/{local_path}"
elif metadata_type == "rpm":
return f"https://rpms.example.com/{local_path}"
else:
return f"https://cdn.example.com/{local_path}"
upgrade_to_v2(
output_dir="/tmp/v2",
url_mapper=my_url_mapper,
images=compose.images,
rpms=compose.rpms,
)
Or via the CLI with a JSON template file:
{
"image": "https://images.example.com/{path}",
"rpm": "https://rpms.example.com/{path}",
"default": "https://cdn.example.com/{path}"
}
productmd upgrade --output /tmp/v2 --url-map templates.json /mnt/compose
Downgrading back to v1.2
v2.0 metadata can be converted back to v1.2 at any time. The
local_path field is used as the path value in the v1.2 output:
productmd downgrade --output /tmp/v1 images.json
Programmatically:
from productmd.convert import downgrade_to_v1
downgrade_to_v1(
output_dir="/tmp/v1",
images=images,
rpms=rpms,
)
Verifying integrity
After localization or manual download, verify that local files match the metadata checksums:
# Quick check (metadata only)
productmd verify --quick images.json
# Full check (verifies all artifact checksums)
productmd verify /mnt/local-compose
# Save results to JSON
productmd verify --report results.json /mnt/local-compose
Programmatically, use the Location’s verify methods:
from productmd.location import Location
loc = image.location
if loc.verify("/mnt/compose/Server/x86_64/iso/boot.iso"):
print("Checksum and size match")
See Also
ProductMD 2.0 Format Specification – Complete v2.0 format specification
Distributed Composes: Use Cases – Use cases and deployment patterns
productmd – CLI tool reference