add library for modifying ISO files
This commit is contained in:
402
udib/modiso.py
Normal file
402
udib/modiso.py
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
"""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
|
||||||
|
|
||||||
|
|
||||||
|
# contains the names of all dependencies (unix programs) which must be
|
||||||
|
# installed on the local system and available in the local system's $PATH
|
||||||
|
# in order for all functions in this module to work
|
||||||
|
system_programs_required = [
|
||||||
|
"bsdtar",
|
||||||
|
"chmod",
|
||||||
|
"cpio",
|
||||||
|
"dd",
|
||||||
|
"find",
|
||||||
|
"gunzip",
|
||||||
|
"gzip",
|
||||||
|
"md5sum",
|
||||||
|
"xargs",
|
||||||
|
"xorriso",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class MissingDependencyError(RuntimeError):
|
||||||
|
"""Raised when a dependency is missing on the local system."""
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def assert_system_dependencies_installed():
|
||||||
|
"""Asserts that all system dependencies are installed.
|
||||||
|
|
||||||
|
System dependencies are those programs whose names are listed in
|
||||||
|
the global 'system_programs_required' variable.
|
||||||
|
|
||||||
|
Raises
|
||||||
|
------
|
||||||
|
MissingDependencyError
|
||||||
|
Raised when a required program is not accessible in the system
|
||||||
|
$PATH.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
for program_name in system_programs_required:
|
||||||
|
try:
|
||||||
|
subprocess.run(["command", "-v", program_name], check=True)
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
raise MissingDependencyError(
|
||||||
|
f"Program not installed or not in $PATH: "
|
||||||
|
f"'{program_name}'.")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_iso(path_to_output_dir, path_to_input_file):
|
||||||
|
"""Extracts the input ISO-file into the specified directory using 'bsdtar'.
|
||||||
|
|
||||||
|
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 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(
|
||||||
|
[
|
||||||
|
"bsdtar",
|
||||||
|
"-C",
|
||||||
|
str(path_to_output_dir),
|
||||||
|
"-xf",
|
||||||
|
str(path_to_input_file)
|
||||||
|
],
|
||||||
|
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
|
||||||
|
# FIXME do this in python, dd is too dangerous
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["dd", f"if={path_to_source_iso}", "bs=1", "count=432",
|
||||||
|
f"of={path_to_output_file}"],
|
||||||
|
shell=True,
|
||||||
|
check=True)
|
||||||
|
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
raise RuntimeError(f"Failed while extracting MBR from source file: "
|
||||||
|
f"'{path_to_source_iso}'.")
|
||||||
|
|
||||||
|
|
||||||
|
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}'.")
|
||||||
62
udib/udib.py
62
udib/udib.py
@@ -1,63 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
from pathlib import Path
|
"""Main entry point for the interactive udib CLI tool."""
|
||||||
import subprocess
|
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
|
||||||
def extract_iso(path_to_output_dir, path_to_input_file):
|
import modiso
|
||||||
"""Extracts the input ISO-file into the specified directory using 'bsdtar'.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
path_to_output_dir : str or pathlike object
|
|
||||||
Path to the directory into which the contents of the archive will be
|
|
||||||
extracted.
|
|
||||||
path_to_input_file : str or pathlike object
|
|
||||||
Path to the file/archive which should be extracted.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
RuntimeError
|
|
||||||
Raised if 'bsdtar' is not installed or encountered an error during the
|
|
||||||
extraction.
|
|
||||||
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}'.")
|
|
||||||
|
|
||||||
# check if 'bsdtar' is installed
|
|
||||||
try:
|
|
||||||
subprocess.run(["command", "-v", "bsdtar"], check=True)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
raise RuntimeError("Program not found in $PATH: 'bsdtar'.")
|
|
||||||
|
|
||||||
# extract file to destination
|
|
||||||
try:
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
"bsdtar",
|
|
||||||
"-C",
|
|
||||||
str(path_to_output_dir),
|
|
||||||
"-xf",
|
|
||||||
str(path_to_input_file)
|
|
||||||
],
|
|
||||||
check=True
|
|
||||||
)
|
|
||||||
except subprocess.CalledProcessError:
|
|
||||||
raise RuntimeError(
|
|
||||||
f"An error occurred while extracting '{path_to_input_file}'."
|
|
||||||
)
|
|
||||||
|
|||||||
Reference in New Issue
Block a user