diff --git a/README.md b/README.md index a1cdaf5..9bd7b27 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,9 @@ # UDIB UDIB is the Unattended Debian Installation Builder. -It provides a handy commandline utility for creating preseeded Debian installation ISOs. -Preseeded ISOs allow partially or fully automated Debian installations on bare metal (or anywhere else). +It provides a handy commandline utility for injecting files into Debian installation ISOs. +Using UDIB, you can preseed an ISO by injecting a preseed file. +Preseeded ISOs allow fully automated Debian installations on bare metal (or anywhere else). ## Quick Start Guide @@ -12,9 +13,9 @@ This short guide explains how to build a Debian ISO with a customized and automa 2. clone this repo and `cd` into your local copy 3. (optional) create and activate a virtual environment: `python3 -m venv .venv && . .venv/bin/activate` 4. install the required python packages: `pip install --user -r requirements.txt` -5. get an example preseed file: `./udib.py -o my-preseed.cfg get preseed-file-basic` -6. customize your installation by editing `my-preseed.cfg` (the comments are pretty self-explanatory) -7. create a Debian ISO with your preseed file: `./udib.py -o my-image.iso inject my-preseed.cfg` +5. download an example preseed file: `./udib.py -o preseed.cfg get preseed-file-basic` +6. customize your installation by editing `preseed.cfg` (the comments are pretty self-explanatory) +7. create a Debian ISO with your preseed file: `./udib.py -o my-image.iso inject preseed.cfg` 8. boot from your newly created ISO `my-image.iso` on your target machine (or in a VM) 9. in the Debian installer menu, navigate to *Advanced options > Automated install* 10. drink some coffee @@ -35,23 +36,12 @@ If you want to know more, you can have a look at Debian's [official guide](https ## How does UDIB work? -UDIB's main purpose is the injection of preseed files into existing Debian installation ISOs. -In a nutshell, it does this by extracting the ISO, adding the preseed file to it, and repacking the ISO again. +UDIB's main purpose is the injection files into existing Debian installation ISOs. +In a nutshell, it does this by extracting the ISO, adding the files to the ISO's initrd, and repacking the ISO again. You could do all of this manually of course by following the [basic](https://wiki.debian.org/DebianInstaller/Preseed/EditIso#Adding_a_Preseed_File_to_the_Initrd) and [advanced](https://wiki.debian.org/RepackBootableISO) guides for ISO repacking on the Debian wiki, but UDIB does all of this for you. # Dependencies -UDIB has some hard- and software requirements, both regarding the [target machine](#target-machine) for which it can build ISOs, as well as on the [build machine](#build-machine) on which UDIB is run. - -## Target Machine - -Images built using UDIB can be used to install Debian on systems satisfying these requirements: - -- **CPU Architecture:** x86 (64-Bit) -- **Network:** an ethernet connection with internet access - -## Build Machine - Using UDIB to create ISOs requires the following software: - GNU/Linux @@ -90,13 +80,15 @@ udib.py [--output-file FILE | --output-dir DIR] get WHAT - `preseed-file-full` to download Debian's full example preseed file (has a LOT of customization options) - `iso` to download the latest, unmodified Debian stable x86-64 netinst ISO -## Creating a preseeded ISO +## Injecting files into an ISO -To inject an existing preseed file into an ISO, you can run the following command: +To inject existing files into an ISO, you can run the following command: ``` -udib.py [--output-file FILE | --output-dir DIR] inject PRESEEDFILE [--image-file IMAGEFILE] +udib.py [--output-file FILE | --output-dir DIR] inject [--image-file IMAGEFILE] FILE [FILE ...] ``` -where `PRESEEDFILE` is the path to your preseed file. -If you don't specify an `--image-file`, UDIB will download the latest Debian x86-64 netinst ISO and inject your `PRESEEDFILE` into it. +where `FILE` is the path to the file you want to inject. +Injected files are added at the root of the installer's filesystem and can be accessed there during the installation. +**NOTE:** the installer will not recognize a preseed file unless it's filename is `preseed.cfg` exactly. +If you don't specify an `--image-file`, UDIB will download the latest Debian x86-64 netinst ISO and inject your `FILE`s into it. diff --git a/cli/parser.py b/cli/parser.py index de57e58..0ca953e 100644 --- a/cli/parser.py +++ b/cli/parser.py @@ -62,11 +62,12 @@ def get_argument_parser(): # register arguments for the 'inject' subcommand subparser_inject.add_argument( - "PRESEEDFILE", + "FILES", action='store', type=str, - metavar='PRESEEDFILE', - help="Path to the preseed file you want to inject", + nargs='+', + metavar='FILES', + help="Paths to all input files you want to inject", ) subparser_inject.add_argument( "-i", diff --git a/core/utils.py b/core/utils.py index aa6f820..d7e1256 100644 --- a/core/utils.py +++ b/core/utils.py @@ -37,7 +37,7 @@ def hash_user_password(printer=None): return p.info("Password hash:") - p.info(crypt(password, crypt.METHOD_SHA512)) + p.info(crypt(password, METHOD_SHA512)) def assert_system_dependencies_installed(): """Checks whether all system dependencies required by udib are installed. diff --git a/iso/injection.py b/iso/injection.py index a6067ca..9d6adfe 100644 --- a/iso/injection.py +++ b/iso/injection.py @@ -149,19 +149,24 @@ def append_file_contents_to_initrd_archive( # NOTE cpio must be called from within the input file's parent # directory, and the input file's name is piped into it completed_process = subprocess.Popen( - ["cpio", "-H", "newc", "-o", "-A", - "-F", str(path_to_initrd_extracted.resolve())], + [ + "cpio", "-H", "newc", "-o", "-A", + "-F", str(path_to_initrd_extracted.resolve()) + ], cwd=path_to_input_file.resolve().parent, stdin=subprocess.PIPE, stdout=subprocess.PIPE, - stderr=subprocess.PIPE) + stderr=subprocess.PIPE + ) completed_process.communicate( - input=str(path_to_input_file.name).encode()) + input=str(path_to_input_file.name).encode() + ) except subprocess.CalledProcessError: - raise RuntimeError(f"Failed while appending contents of " - f"'{path_to_input_file}' to " - f"'{path_to_initrd_archive}'.") + raise RuntimeError( + f"Failed while appending contents of '{path_to_input_file}' to " + f"'{path_to_initrd_archive}'." + ) # repack archive with gzip.open(path_to_initrd_archive, "wb") as file_gz: @@ -412,17 +417,17 @@ def repack_iso(path_to_output_iso, f"'{path_to_input_files_root_dir}'.") -def inject_preseed_file_into_iso( +def inject_files_into_iso( path_to_output_iso_file, path_to_input_iso_file, - path_to_input_preseed_file, + input_file_paths, iso_filesystem_name="Debian", printer=None, ): - """Injects the specified preseed file into the specified ISO file. + """Injects the specified input files into the specified ISO file. Extracts the input ISO into a temporary directory, then extracts the input - ISO's MBR into a temporary file, then appends the preseed file to the + ISO's MBR into a temporary file, then appends the input files to the extracted ISO's initrd, then regenerates the extracted ISO's internal MD5 hash list and finally repacks the extracted ISO into the output ISO. @@ -435,8 +440,8 @@ def inject_preseed_file_into_iso( Path to which the resulting ISO file will be saved. path_to_input_iso_file : str or pathlike object Path to the origin ISO file. - path_to_input_preseed_file : str or pathlike object - Path to the input preseed file. + input_file_paths : list containing str or pathlike objects + A list of paths to the input files. printer : clibella.Printer A printer for CLI output. """ @@ -456,11 +461,16 @@ def inject_preseed_file_into_iso( if not path_to_output_iso_file.parent.is_dir(): raise NotADirectoryError(f"No such directory: '{path_to_output_iso_file.parent}'.") - if "~" in str(path_to_input_preseed_file): - path_to_input_preseed_file = Path(path_to_input_preseed_file).expanduser() - path_to_input_preseed_file = Path(path_to_input_preseed_file).resolve() - if not path_to_input_preseed_file.is_file(): - raise FileNotFoundError(f"No such file: '{path_to_input_preseed_file}'.") + input_file_paths_old = input_file_paths.copy() + input_file_paths = [] + for path in input_file_paths_old: + if "~" in str(path): + path = Path(path).expanduser() + path = Path(path).resolve() + if not path.is_file(): + raise FileNotFoundError(f"No such file: '{path}'.") + input_file_paths.append(path) + del input_file_paths_old if printer is None: p = Printer() @@ -490,18 +500,18 @@ def inject_preseed_file_into_iso( ) p.ok("MBR extraction complete.") - # create a correctly named copy of the input preseed file and append it - # to the extracted ISO's initrd - p.info(f"Appending {path_to_input_preseed_file.name} to initrd...") - temp_preseed_dir = TemporaryDirectory() - path_to_preseed_dir = Path(temp_preseed_dir.name) - path_to_preseed_file = path_to_preseed_dir/"preseed.cfg" - shutil.copy(path_to_input_preseed_file, path_to_preseed_file) - append_file_contents_to_initrd_archive( - path_to_extracted_iso_dir/"install.amd"/"initrd.gz", - path_to_preseed_file - ) - p.ok("Preseed file appended successfully.") + # append all input files to the extracted ISO's initrd + temp_file_dir = TemporaryDirectory() + path_to_file_dir = Path(temp_file_dir.name) + for path_to_file in input_file_paths: + p.info(f"Appending {path_to_file.name} to initrd...") + path_to_file_copy = path_to_file_dir/path_to_file.name + shutil.copy(path_to_file, path_to_file_copy) + append_file_contents_to_initrd_archive( + path_to_extracted_iso_dir/"install.amd"/"initrd.gz", + path_to_file_copy + ) + p.ok(f"{path_to_file.name} appended successfully.") # regenerate extracted ISO's md5sum.txt file p.info("Regenerating MD5 checksums...") @@ -519,6 +529,6 @@ def inject_preseed_file_into_iso( p.success(f"ISO file was created successfully at '{path_to_output_iso_file}'.") # clear out temporary directories - temp_preseed_dir.cleanup() + temp_file_dir.cleanup() temp_mbr_dir.cleanup() temp_extracted_iso_dir.cleanup() diff --git a/udib.py b/udib.py index cd6fde8..adce9bf 100755 --- a/udib.py +++ b/udib.py @@ -8,7 +8,7 @@ from tempfile import TemporaryDirectory from cli.clibella import Printer from cli.parser import get_argument_parser from core.utils import assert_system_dependencies_installed, download_and_verify_debian_iso -from iso.injection import inject_preseed_file_into_iso +from iso.injection import inject_files_into_iso from net.download import download_file from net.scrape import get_debian_preseed_file_urls, get_debian_iso_urls @@ -112,20 +112,23 @@ def main(): elif args.subparser_name == "inject": image_file_name = Path(get_debian_iso_urls()["image_file"]["name"]) if not path_to_output_file: - output_file_name = image_file_name.stem + "-preseeded" + image_file_name.suffix + output_file_name = image_file_name.stem + "-modified" + image_file_name.suffix if path_to_output_dir: path_to_output_file = path_to_output_dir / output_file_name else: path_to_output_file = Path.cwd() / output_file_name - # verify preseed file path - path_to_preseed_file = Path(args.PRESEEDFILE) - if "~" in str(path_to_preseed_file): - path_to_preseed_file = Path(path_to_preseed_file).expanduser() - path_to_preseed_file = Path(path_to_preseed_file).resolve() - if not path_to_preseed_file.is_file(): - p.error(f"No such file: '{path_to_preseed_file}'.") - exit(1) + # verify input file paths + input_file_paths = [] + for path in args.FILES: + path = Path(path) + if "~" in str(path): + path = Path(path).expanduser() + path = Path(path).resolve() + if not path.is_file(): + p.error(f"No such file: '{path}'.") + exit(1) + input_file_paths.append(path) # verify image file path if set by user or download fresh iso if unset temp_iso_dir = None @@ -145,11 +148,11 @@ def main(): path_to_image_file = path_to_iso_dir/image_file_name download_and_verify_debian_iso(path_to_image_file, printer=p) - # inject the preseed file - inject_preseed_file_into_iso( + # inject the input files + inject_files_into_iso( path_to_output_file, path_to_image_file, - path_to_preseed_file, + input_file_paths, printer=p, )