#!/usr/bin/env python3 """Main entry point for the interactive udib CLI tool.""" from pathlib import Path import argparse import os import shutil import subprocess import sys import tempfile import clibella import modiso import userinput import webdownload p = clibella.Printer() 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 ------ RuntimeError Raised when a required program is not accessible in the system $PATH. """ # contains the names of all unix program dependencies which must be # installed on the local system and available in the local system's $PATH system_programs_required = [ "cpio", "xorriso", ] for program_name in system_programs_required: try: subprocess.run( ["command", "-v", program_name], shell=True, check=True) except subprocess.CalledProcessError: raise RuntimeError( f"Program not installed or not in $PATH: " f"'{program_name}'.") def _get_argument_parser(): """Instantiates and configures an ArgumentParser and returns it. Examples -------- parser = _get_argument_parser() args = parser.parse_args() """ # FIXME add a group hierarchy # FIXME capture ISO filesystem name parser = argparse.ArgumentParser() # create a group of mutually exclusive arguments mutually_exclusive_group = parser.add_mutually_exclusive_group( required=True) mutually_exclusive_group.add_argument( "--get-image", help="Download the latest, unmodified Debian image and exit.", action="store_true") mutually_exclusive_group.add_argument( "--get-preseed-file", help="Download the latest Debian preseed example file and exit.", action="store_true") mutually_exclusive_group.add_argument( "-p", "--existing-preseed-file", help="Path to the preseed configuration file to use.", action="store") # add all other arguments parser.add_argument( "-o", "--output-file", help="Path to which the resulting file is written.", type=str, dest="path_to_output_file", action="store") parser.add_argument( "-i", "--existing-image", help="Use an existing debian image file, do not download a new one.", type=str, dest="path_to_existing_image", action="store") return parser def _chmod_recursively(input_path, mode): """Recursively changes file permissions on the specified path. The file and directory permissions for the specified path itself, as well as any files and directories anywhere below it are set to the specified mode. Symlinks are ignored. Parameters ---------- input_path : str or pathlike object Path of which to change the permissions recursively. mode : int Permission mode as accepted by os.chmod() Raises ------ ValueError Raised if the specified path points to a nonexisting filesystem node, or if the specified permission mode is invalid. TypeError Raised if mode is not an integer. Examples -------- _chmod_recursively("/tmp/mydir", 0o755) """ input_path = Path(input_path) if not input_path.exists(): raise ValueError(f"Path does not exist: '{input_path}'.") if not isinstance(mode, int): raise TypeError("Expected type 'int' for parameter 'mode'.") if mode not in range(0, 0o1000): raise ValueError("Invalid mode.") if not input_path.is_symlink(): input_path.chmod(mode) for child in input_path.iterdir(): if not child.is_symlink(): child.chmod(mode) if child.is_dir(): _chmod_recursively(child, mode) def main(): # FIXME capture ISO filesystem name iso_filesystem_name = "Debian UDIB" parser = _get_argument_parser() args = parser.parse_args() # check if all system dependencies are installed try: _assert_system_dependencies_installed() except RuntimeError as e: p.error(e) sys.exit(1) # determine where to output files # default to ouputting files to the current directory # using their original name if not specified by '-o' argument path_to_output_dir = Path.cwd() path_to_output_file = None # check if path_to_output_file is valid and adjust path_to_output_dir if # '-o' was set if args.path_to_output_file: path_to_output_file = Path(args.path_to_output_file).resolve() if path_to_output_file.exists() and not path_to_output_file.is_file(): p.error(f"Specified output location is not a file: " f"'{path_to_output_file}'.") sys.exit(1) path_to_output_dir = path_to_output_file.parent if args.get_image: # download latest image and exit with tempfile.TemporaryDirectory() as tmp_dir: # save image to a temporary directory path_to_image_file = webdownload.debian_obtain_image( tmp_dir) if path_to_output_file: # rename file if '-o' flag was specified path_to_image_file = path_to_image_file.rename( path_to_output_file.name) else: # else determine the intended final path path_to_output_file = path_to_output_dir/path_to_image_file.name # prompt to confirm file removal if output file already exists if path_to_output_file.is_file(): prompt = f"File '{path_to_output_file}' already exists. " \ f"Overwrite it?" if userinput.prompt_yes_or_no(prompt, ask_until_valid=True): os.remove(path_to_output_file) else: p.failure("Did not obtain a new image file.") sys.exit(1) # move file from temporary directory to intended path path_to_image_file.rename(path_to_output_file) sys.exit(0) elif args.get_preseed_file: # download latest preseed file and exit preseed_file_name = "example-preseed.txt" preseed_file_url = "https://www.debian.org/"\ "releases/stable/" + preseed_file_name if not path_to_output_file: path_to_output_file = path_to_output_dir/preseed_file_name # prompt to confirm file removal if output file already exists if path_to_output_file.is_file(): prompt = f"File '{path_to_output_file}' already exists. " \ f"Overwrite it?" if userinput.prompt_yes_or_no(prompt, ask_until_valid=True): os.remove(path_to_output_file) else: p.failure("Did not obtain a new preseed file.") sys.exit(1) webdownload.download_file(path_to_output_file, preseed_file_url) sys.exit(0) else: # modify image file using specified preseed file path_to_preseed_file = Path(args.existing_preseed_file) if not path_to_preseed_file.is_file(): p.error(f"No such file: '{path_to_preseed_file}'.") sys.exit(1) if args.path_to_existing_image: # user has specified an existing image file path_to_image_file = Path(args.path_to_existing_image) # sanity check if path_to_image_file.suffix not in [".iso", ".img"]: p.warning(f"Specified image file does not appear to be an ISO " f"or IMG file: '{path_to_image_file}'!") if not userinput.prompt_yes_or_no("Proceed anyways?"): p.failure("Did not create a new image file.") sys.exit(1) else: # download a fresh image file to a temporary directory # remember to delete it later! path_to_image_file = webdownload.debian_obtain_image( tempfile.mkdtemp()) # extract image file to a temporary directory path_to_extracted_iso_dir = Path(tempfile.mkdtemp()) p.info("Extracting ISO contents...") modiso.extract_iso( path_to_extracted_iso_dir, path_to_image_file) # extract image MBR to a temporary directory p.info("Extracting master boot record...") path_to_mbr = Path(tempfile.mkdtemp())/"mbr.bin" modiso.extract_mbr_from_iso( path_to_mbr, path_to_image_file) # append preseed file to extracted initrd p.info("Appending preseed file...") # make a temporary copy called 'preseed.cfg' with tempfile.TemporaryDirectory() as tmpdir: path_to_renamed_preseed_file = Path(tmpdir).resolve()/"preseed.cfg" shutil.copy(path_to_preseed_file, path_to_renamed_preseed_file) modiso.append_file_contents_to_initrd_archive( path_to_extracted_iso_dir/"install.amd"/"initrd.gz", path_to_renamed_preseed_file) # regenerate md5sum.txt p.info("Regenerating MD5 checksum...") modiso.regenerate_iso_md5sums_file(path_to_extracted_iso_dir) if not path_to_output_file: # use original filename with "-udib" appended before the extension path_to_output_file = (path_to_output_dir / (path_to_image_file.with_suffix("").name + "-udib" + path_to_image_file.suffix)) # check if outputfile already exists if path_to_output_file.is_file(): prompt = f"File '{path_to_output_file}' already exists. " \ f"Overwrite it?" if userinput.prompt_yes_or_no(prompt, ask_until_valid=True): os.remove(path_to_output_file) else: p.failure("Did not create a new ISO file.") sys.exit(1) # repack ISO file p.info("Repacking ISO file...") modiso.repack_iso( path_to_output_file, path_to_mbr, path_to_extracted_iso_dir, iso_filesystem_name) # remove temporary directories p.info("Cleaning up...") _chmod_recursively(path_to_extracted_iso_dir, 0o755) shutil.rmtree(path_to_extracted_iso_dir) _chmod_recursively(path_to_mbr.parent, 0o755) shutil.rmtree(path_to_mbr.parent) p.success(f"Wrote the modified ISO to '{path_to_output_file}'.") if __name__ == "__main__": main()