Files
custom-debian-iso/udib/modiso.py
ulinja df3c9c58d3 replace ISO extraction with xorriso-implementation
bsdtar is no longer required as a dependency
2022-04-22 15:33:03 +02:00

347 lines
12 KiB
Python

"""Library for modification of disk image files.
Image file modification includes extracting ISO archives, adding files to
initrd-archives contained within the ISO, recalculating md5sum-files
inside the ISO and rebuilding bootable ISOs from directories on the
local filesystem.
"""
from pathlib import Path
import re
import subprocess
def extract_iso(path_to_output_dir, path_to_input_file):
"""Extracts the contents of the ISO-file into the specified directory.
Source: https://wiki.debian.org/DebianInstaller/Preseed/EditIso#Extracting_the_Initrd_from_an_ISO_Image
Parameters
----------
path_to_output_dir : str or pathlike object
Path to the directory into which the contents of the ISO archive will
be extracted.
path_to_input_file : str or pathlike object
Path to the file/archive which should be extracted.
Raises
------
FileNotFoundError
Raised if the input file does not exist or is not a file.
NotADirectoryError
Raised if the output directory does not exist or is not a directory.
Example
-------
extract_iso("/tmp/isocontents", "/home/myuser/downloads/debian-11.iso")
"""
path_to_output_dir = Path(path_to_output_dir)
path_to_input_file = Path(path_to_input_file)
# check if paths are valid
if not path_to_output_dir.is_dir():
raise NotADirectoryError(f"No such directory: '{path_to_output_dir}'.")
if not path_to_input_file.is_file():
raise FileNotFoundError(f"No such file: '{path_to_input_file}'.")
# extract file to destination
try:
subprocess.run(
[
"xorriso",
"-osirrox", "on"
"-indev", path_to_input_file.resolve(),
"-extract", "/",
path_to_output_dir.resolve()
],
check=True)
except subprocess.CalledProcessError:
raise RuntimeError(
f"An error occurred while extracting '{path_to_input_file}'.")
def append_file_contents_to_initrd_archive(path_to_initrd_archive,
path_to_input_file):
"""Appends the input file to the specified initrd archive.
The initrd archive is extracted, the input file is appended, and
the initrd is repacked again.
Source: https://wiki.debian.org/DebianInstaller/Preseed/EditIso#Adding_a_Preseed_File_to_the_Initrd
Parameters
----------
path_to_initrd_archive : str or pathlike object
Path the the initrd archive to which the input file shall be
appended. The initrd file must be called 'initrd.gz'.
path_to_input_file : str or pathlike object
Path to the input file which shall be added to the initrd
archive.
Raises
------
AssertionError
Thrown if the initrd archive is not named 'initrd.gz'.
FileNotFoundError
Thrown if the initrd archive file or input file does not
exist.
Examples
--------
append_file_contents_to_initrd_archive(
"/tmp/isofiles/install.386/initrd.gz",
"/tmp/preseed.cfg")
"""
path_to_initrd_archive = Path(path_to_initrd_archive)
path_to_input_file = Path(path_to_input_file)
# check if initrd file exists and has the correct name
if not path_to_initrd_archive.is_file():
raise FileNotFoundError(f"No such file: '{path_to_initrd_archive}'.")
if not path_to_initrd_archive.name == "initrd.gz":
raise AssertionError(f"Does not seem to be an initrd.gz archive: "
f"'{path_to_initrd_archive.name}'.")
# check if input file exists
if not path_to_input_file.is_file():
raise FileNotFoundError(f"No such file: '{path_to_input_file}'.")
# temporarily extract initrd archive and append the input file's contents
try:
# make archive temporarily writable
subprocess.run(["chmod", "+w", path_to_initrd_archive],
check=True)
# extract archive in-place
subprocess.run(["gunzip", path_to_initrd_archive],
check=True)
# append contents of input_file to extracted archive using cpio
subprocess.run(
["echo", path_to_input_file,
"|", "cpio", "-H", "newc", "-o", "-A",
"-F", path_to_initrd_archive.with_suffix("")],
shell=True,
check=True)
# repack archive
subprocess.run(
["gzip", path_to_initrd_archive.with_suffix("")],
check=True)
# remove write permissions from repacked archive
subprocess.run(["chmod", "-w", path_to_initrd_archive],
check=True)
except subprocess.CalledProcessError:
raise RuntimeError(f"Failed while appending contents of "
f"'{path_to_input_file}' to "
f"'{path_to_initrd_archive}'.")
def regenerate_iso_md5sums_file(path_to_extracted_iso_root):
"""Recalculates and rewrites the md5sum.txt file for the extracted ISO.
Source: https://wiki.debian.org/DebianInstaller/Preseed/EditIso#Regenerating_md5sum.txt
Parameters
----------
path_to_extracted_iso_root : str or pathlike object
Path to the root folder containing an extracted ISO's
contents.
Raises
------
RuntimeError
Raised if the recalculation/rewrite operation fails.
NotADirectoryError
Raised if the specified directory is not a directory or does
not exist.
Examples
--------
regenerate_iso_md5sums_file("/tmp/extracted_iso")
"""
path_to_extracted_iso_root = Path(path_to_extracted_iso_root)
# check if input path exists
if not path_to_extracted_iso_root.is_dir():
raise NotADirectoryError(f"No such directory: "
f"'{path_to_extracted_iso_root}'.")
# recalculate and rewrite 'md5sum.txt' in ISO's root
try:
# make md5sum.txt temporarily writable
subprocess.run(
["chmod", "+w", path_to_extracted_iso_root/"md5sum.txt"],
check=True)
# find all files within ISO's root and regenerate md5sum.txt file
subprocess.run(
["find", path_to_extracted_iso_root,
"-follow", "-type", "f", "!", "-name", "md5sum.txt", "-print0"
"|", "xargs", "-0", "md5sum",
">", path_to_extracted_iso_root/"md5sum.txt"],
shell=True,
check=True)
# remove write permissions from md5sum.txt
subprocess.run(
["chmod", "-w", path_to_extracted_iso_root/"md5sum.txt"],
check=True)
except subprocess.CalledProcessError:
raise RuntimeError(f"Failed while regenerating "
f"'md5sum.txt' within "
f"'{path_to_extracted_iso_root}'.")
def extract_mbr_from_iso(path_to_output_file, path_to_source_iso):
"""Extracts the MBR-data from the ISO and writes it into the outputfile.
The source ISO file must be a BIOS-bootable '.iso'- or
'.img'-file. You should use a "vanilla" debian installation ISO as
the source file.
Source: https://wiki.debian.org/RepackBootableISO#Determine_those_options_which_need_to_be_adapted_on_amd64_or_i386
Parameters
----------
path_to_output_file : str or pathlike object
Path to the file which will be created and contain the MBR
data.
path_to_source_iso : str or pathlike object
Path to the source ISO whose MBR data will get extracted.
Raises
------
RuntimeError
Raised if the input ISO has the wrong file extension.
FileNotFoundError
Raised if the input ISO does not exist or is not a file.
FileExistsError
Raised if the output file already exists.
Examples
--------
extract_mbr_from_iso("/tmp/mbr-data.bin", "/tmp/debian-11.0.4-netinst.iso")
"""
path_to_output_file = Path(path_to_output_file)
path_to_source_iso = Path(path_to_source_iso)
# make sure output file does not exist already
if path_to_output_file.exists():
raise FileExistsError(
f"Outputfile exists and would get overwritten: "
f"'{path_to_output_file}'.")
# make sure input file exists and has the right extension
if not path_to_source_iso.is_file():
raise FileNotFoundError(f"No such file: '{path_to_source_iso}'.")
if path_to_source_iso.suffix not in [".iso", ".img"]:
raise RuntimeError(
f"Input file is not an image file: '{path_to_source_iso}'.")
# extract the MBR (first 432 Bytes) of the source ISO file
with open(path_to_source_iso, mode="r+b") as iso_file:
with open(path_to_output_file, mode="w+b") as mbr_file:
mbr_file.write(iso_file.read(432))
def repack_iso(path_to_output_iso,
path_to_mbr_data_file,
path_to_input_files_root_dir,
created_iso_filesystem_name):
"""Rebuilds a bootable ISO image using the input files.
The input files root directory contains the contents of a previously
extracted ISO file, with its contents possibly modified.
The MBR data file used should contain MBR data extracted from the
originially extracted ISO.
The given filesystem name written into the modified ISO appears when
the ISO gets mounted. It may only contain alphanumeric characters,
hyphens, underscores or periods.
Source: https://wiki.debian.org/RepackBootableISO#Determine_those_options_which_need_to_be_adapted_on_amd64_or_i386
Parameters
----------
path_to_output_iso : str or pathlike object
Path to the file as which the created ISO file will be saved.
path_to_mbr_data_file : str or pathlike object
Path to an existing file containing MBR data.
path_to_input_files_root_dir : str or pathlike object
Path to the root directory of those files which will be repacked into
the new ISO file.
created_iso_filesystem_name : str
Name of the filesystem which the created ISO will have upon
mounting it.
Raises
------
RuntimeError
Raised if the ISO packing process fails.
NotADirectoryError
Raised if the specified input files root directory does not
exist or is not a directory.
FileNotFoundError
Raised if the MBR data file does not exist or is not a file.
FileExistsError
Raised if the output file already exists.
Examples
--------
repack_iso("/tmp/debian-11.0.4-modified.iso",
"/tmp/mbr-data.bin",
"/tmp/extracted-iso",
"Debian 11.0.4 installation image")
"""
path_to_output_iso = Path(path_to_output_iso)
path_to_mbr_data_file = Path(path_to_mbr_data_file)
path_to_input_files_root_dir = Path(path_to_input_files_root_dir)
# make sure output file does not exist yet
if path_to_output_iso.exists():
raise FileExistsError(f"Existing file would get overwritten: "
f"'{path_to_output_iso}'.")
# make sure input files exist
if not path_to_mbr_data_file.is_file():
raise FileNotFoundError(f"No such file: '{path_to_mbr_data_file}'.")
if not path_to_input_files_root_dir.is_dir():
raise NotADirectoryError(f"No such directory: "
f"'{path_to_input_files_root_dir}'.")
# make sure specified filesystem name contains no illegal characters:
# only alphanumeric, ' ', '.', '_' and '-' are allowed.
filesystem_name_invalid_char_regex = re.compile(r"[^\w .-]")
invalid_char_match = filesystem_name_invalid_char_regex.search(
created_iso_filesystem_name)
if invalid_char_match is not None:
raise RuntimeError(f"Invalid character in filesystem name: "
f"'{invalid_char_match.group()[0]}'.")
# repack the ISO using xorriso
try:
subprocess.run(
["xorriso", "-as", "mkisofs",
"-r", "-V", created_iso_filesystem_name,
"-o", path_to_output_iso,
"-J", "-J", "-joliet-long", "-cache-inodes",
"-isohybrid-mbr", path_to_mbr_data_file,
"-b", "isolinux/isolinux.bin",
"-c", "isolinux/boot.cat",
"-boot-load-size", "4", "-boot-info-table", "-no-emul-boot",
"-eltorito-alt-boot",
"-e", "boot/grub/efi.img", "-no-emul-boot",
"-isohybrid-gpt-basdat", "-isohybrid-apm-hfsplus",
path_to_input_files_root_dir],
check=True)
except subprocess.CalledProcessError:
raise RuntimeError(f"Failed while repacking ISO from source files: "
f"'{path_to_input_files_root_dir}'.")