debian-forge-composer/tools/image-info
Lars Karlitski bf3d7fcdca image-info: add basic support for multiple partitions
For each partition, find out if its the root or boot partition and
gather only the relevant information. Make sure that we don't get
information from /boot twice.
2019-09-30 13:32:53 +02:00

118 lines
3.6 KiB
Python
Executable file

#!/usr/bin/python3
import contextlib
import glob
import json
import os
import subprocess
import sys
import tempfile
image = sys.argv[1]
subprocess.run(["modprobe", "nbd"], check=True)
@contextlib.contextmanager
def nbd_connect(image):
for device in glob.glob("/dev/nbd*"):
r = subprocess.run(["qemu-nbd", "--connect", device, "--read-only", image], check=False).returncode
if r == 0:
try:
yield device
finally:
subprocess.run(["qemu-nbd", "--disconnect", device], check=True, stdout=subprocess.DEVNULL)
break
else:
raise RuntimeError("no free network block device")
@contextlib.contextmanager
def mount(device):
with tempfile.TemporaryDirectory() as mountpoint:
subprocess.run(["mount", "-o", "ro", device, mountpoint], check=True)
try:
yield mountpoint
finally:
subprocess.run(["umount", "--lazy", mountpoint], check=True)
def subprocess_check_output(argv, parse_fn=None):
output = subprocess.check_output(argv, encoding="utf-8")
return parse_fn(output) if parse_fn else output
def read_partition_table(device):
sfdisk = subprocess_check_output(["sfdisk", "--json", device], json.loads)
ptable = sfdisk["partitiontable"]
assert ptable["unit"] == "sectors"
partitions = []
for p in ptable["partitions"]:
partitions.append({
"type": p["type"],
"bootable": p.get("bootable", False),
"start": p["start"] * 512,
"size": p["size"] * 512
})
return ptable["label"], partitions
def read_bootloader_type(device):
with open(device, "rb") as f:
if b"GRUB" in f.read(512):
return "grub"
else:
return "unknown"
def read_os_release(tree):
r = {}
with open(f"{tree}/etc/os-release") as f:
for line in f:
key, value = line.strip().split("=")
r[key] = value.strip('"')
return r
def read_bls_conf(filename):
with open(filename) as f:
return dict(line.strip().split(" ", 1) for line in f)
report = {}
with nbd_connect(image) as device:
report["bootloader"] = read_bootloader_type(device)
report["partition_table"], report["partitions"] = read_partition_table(device)
n_partitions = len(report["partitions"])
for n in range(1, n_partitions + 1):
with mount(device + f"p{n}") as tree:
# subprocess.run(["ls", "-l", tree])
if os.path.exists(f"{tree}/etc/os-release"):
report["packages"] = sorted(subprocess_check_output(["rpm", "--root", tree, "-qa"], str.split))
report["os_release"] = read_os_release(tree)
with open(f"{tree}/etc/fstab") as f:
report["fstab"] = sorted([line.split() for line in f.read().split("\n") if line and not line.startswith("#")])
with open(f"{tree}/etc/passwd") as f:
report["passwd"] = sorted(f.read().strip().split("\n"))
with open(f"{tree}/etc/group") as f:
report["groups"] = sorted(f.read().strip().split("\n"))
if os.path.exists(f"{tree}/boot") and len(os.listdir(f"{tree}/boot")) > 0:
assert "bootmenu" not in report
report["bootmenu"] = [read_bls_conf(f) for f in glob.glob(f"{tree}/boot/loader/entries/*.conf")]
elif len(glob.glob(f"{tree}/vmlinuz-*")) > 0:
assert "bootmenu" not in report
report["bootmenu"] = [read_bls_conf(f) for f in glob.glob(f"{tree}/loader/entries/*.conf")]
json.dump(report, sys.stdout, sort_keys=True, indent=2)