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
|
||||
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
"""Main entry point for the interactive udib CLI tool."""
|
||||
|
||||
import argparse
|
||||
|
||||
def extract_iso(path_to_output_dir, path_to_input_file):
|
||||
"""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}'."
|
||||
)
|
||||
import modiso
|
||||
|
||||
Reference in New Issue
Block a user