98
README.md
Normal file
98
README.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 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).
|
||||
|
||||
## What's preseeding?
|
||||
|
||||
A preseed file is a text file which provides the Debian installer with previously set (preseeded) answers during the installation process.
|
||||
Preseeding a Debian ISO allows you to heavily reduce the amount of user interaction required during an installation, or even fully automate it.
|
||||
The preseed file is written by you and then injected into the installation image.
|
||||
When you start the installation, any answers you have provided to the debian installer as part of your preseed file are automatically applied during the installation.
|
||||
If you want to know more, you can have a look at Debian's [official guide](https://www.debian.org/releases/stable/amd64/apb.en.html) or at the Debian wiki's [quick overview](https://wiki.debian.org/DebianInstaller/Preseed), both of which explain preseeding much better than I can.
|
||||
|
||||
## 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.
|
||||
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
|
||||
- `python3` *(3.10.4 known to work)*
|
||||
- [required python packages](./requirements.txt) can be installed in a virtual environment
|
||||
- `xorriso` *(1.5.4 known to work)*
|
||||
- **Debian (bullseye):** [xorriso](https://packages.debian.org/bullseye/xorriso)
|
||||
- **Arch Linux:** [extra/libisoburn](extra/libisoburn)
|
||||
- `gpg` *(2.2.32 known to work)*
|
||||
- GNU `cpio`
|
||||
- preinstalled on most distributions
|
||||
|
||||
Internet access is (obviously) required if you want to fetch any files using UDIB.
|
||||
|
||||
# Quick Start Guide
|
||||
|
||||
This short guide explains how to build a Debian ISO with a customized and automated installation:
|
||||
|
||||
1. make sure you have all the [required software](#build-machine) installed
|
||||
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`
|
||||
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
|
||||
11. return to your new Debian system
|
||||
|
||||
Depending on how many answers you provided in the preseed file, the installation may require some manual interaction.
|
||||
Preseed files are very powerful, and if you need more customization you can have a deeper look into [how they work](#whats-preseeding).
|
||||
You can also use UDIB to get the full preseed example file: `./udib.py get preseed-file-full` and use that as a starting point.
|
||||
|
||||
# Detailed usage guide
|
||||
|
||||
You can view help at the commandline using `./udib.py --help` for general options and `./udib.py COMMAND --help` for help with a specific subcommand.
|
||||
|
||||
The name and destination of files produced by `udib.py` can be specified with the `--output-file` option.
|
||||
Alternatively, you can use the `--output-dir` option to specify the directory where produced files will be saved, without having to name them explicitly (default names will be used).
|
||||
|
||||
## Retrieving example preseed files or vanilla ISOs
|
||||
|
||||
As a starting point for creating your own preseeded ISO, you can retrieve one of Debian's example preseed files or an unmodified Debian ISO using UDIB:
|
||||
|
||||
```
|
||||
udib.py [--output-file FILE | --output-dir DIR] get WHAT
|
||||
```
|
||||
|
||||
`WHAT` must be a specific string and can be either one of:
|
||||
|
||||
- `preseed-file-basic` to download Debian's basic example preseed file (sufficient in most cases)
|
||||
- `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
|
||||
|
||||
To inject an existing preseed file into an ISO, you can run the following command:
|
||||
|
||||
```
|
||||
udib.py [--output-file FILE | --output-dir DIR] inject PRESEEDFILE [--image-file IMAGEFILE]
|
||||
```
|
||||
|
||||
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.
|
||||
60
README.org
60
README.org
@@ -1,60 +0,0 @@
|
||||
* UDIB
|
||||
|
||||
=UDIB= is the /unattended debian installation (image) builder/.
|
||||
As the name would suggest, it is a tool which builds a debian installation image which, if used to boot from, installs the *latest stable release* of Debian Linux to the machine without any user interaction required whatsoever.
|
||||
Configuration of the installed system is done by editing preseed files.
|
||||
|
||||
** Usage
|
||||
|
||||
Obtaining a preseed example file:
|
||||
|
||||
#+begin_src shell
|
||||
./udib.py get preseed-file
|
||||
#+end_src
|
||||
|
||||
** Prerequisites
|
||||
|
||||
*** Target machine
|
||||
|
||||
Any target machine on which you wish to install Debian using images built using =UDIB= must fulfil the following requirements:
|
||||
|
||||
- *Architecture:* An x64-amd CPU
|
||||
- *Network access:* an ethernet connection with internet access (/wifi does not work/)
|
||||
|
||||
*** Build machine
|
||||
|
||||
=UDIB= has the following dependencies, installed on and in the ~$PATH~ of the machine on which you build your image:
|
||||
|
||||
- =python3= /(3.10.4 known to work)/
|
||||
|
||||
- =xorriso= /(1.5.4 known to work)/
|
||||
- part of [[https://packages.debian.org/bullseye/xorriso][xorriso]] on Debian bullseye
|
||||
- part of [[https://www.archlinux.org/packages/extra/x86_64/libisoburn/][extra/libisoburn]] on Arch Linux
|
||||
|
||||
- =gpg= /(2.2.32 known to work)/
|
||||
|
||||
- GNU =cpio=
|
||||
- preinstalled on most distributions
|
||||
|
||||
Internet access is required so that =UDIB= can download the Debian installation image.
|
||||
|
||||
** How it works
|
||||
|
||||
=UDIB= does the following:
|
||||
|
||||
1. download the latest debian stable installation ISO
|
||||
2. verify integrity and authenticity of the obtained ISO
|
||||
3. extract the ISO
|
||||
4. inject the preseed configuration into the ISO's initrd
|
||||
5. repack the modified ISO
|
||||
|
||||
*** Preseeding
|
||||
|
||||
The Debian project provides a well-written and detailed [[https://www.debian.org/releases/stable/amd64/apb.en.html][official guide]] on how preseeding works, and how to implement it. The Debian wiki offers a [[https://wiki.debian.org/DebianInstaller/Preseed][quick overview]] of preseeding as well.
|
||||
|
||||
Example preseeding configurations are provided as well. The [[https://www.debian.org/releases/stable/example-preseed.txt][basic example]] is what =UDIB= is based on, but much more detailed preseeding is also possible, as seen in the [[https://preseed.debian.net/debian-preseed/bullseye/amd64-main-full.txt][full example]].
|
||||
|
||||
*** ISO repacking
|
||||
|
||||
The Debian wiki provides both a [[https://wiki.debian.org/DebianInstaller/Preseed/EditIso#Adding_a_Preseed_File_to_the_Initrd][basic guide]] and a [[https://wiki.debian.org/RepackBootableISO][detailed guide]] on repacking debian installation images with the preseed file (and other files) contained within.
|
||||
|
||||
0
__init__.py
Normal file
0
__init__.py
Normal file
0
cli/__init__.py
Normal file
0
cli/__init__.py
Normal file
@@ -1,6 +1,7 @@
|
||||
"""Library for consistent and visually appealing terminal output."""
|
||||
|
||||
import sys
|
||||
from re import compile
|
||||
|
||||
import colorama
|
||||
from colorama import Fore, Style
|
||||
@@ -335,3 +336,45 @@ class Printer:
|
||||
color_enabled=color_enabled,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
def prompt_yes_or_no(self, question: str, ask_until_valid: bool = False):
|
||||
"""Prompts the user for an answer to the specified yes/no question.
|
||||
|
||||
If 'ask_until_valid' is True, keeps repeating the prompt until the user
|
||||
provides recognizable input to answer the question.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
question : str
|
||||
The question to ask the user while prompting. The string is suffixed
|
||||
with " (Yes/No) ".
|
||||
ask_until_valid : bool
|
||||
If this is set to True and the user provides an ambiguous answer, keeps
|
||||
prompting indefinitely until an unambiguous answer is read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
True : bool
|
||||
If the user has answered 'yes'.
|
||||
False : bool
|
||||
If the user has answered 'no'.
|
||||
None : None
|
||||
If 'ask_until_valid' is False and the user has provided an ambiguous
|
||||
response to the prompt.
|
||||
"""
|
||||
|
||||
user_input_is_valid = False
|
||||
|
||||
regex_yes = compile(r"^(y)$|^(Y)$|^(YES)$|^(Yes)$|^(yes)$")
|
||||
regex_no = compile(r"^(n)$|^(N)$|^(NO)$|^(No)$|^(no)$")
|
||||
|
||||
while(not user_input_is_valid):
|
||||
user_input = self.input(f"{question} (Yes/No): ")
|
||||
|
||||
if (regex_yes.match(user_input)):
|
||||
return True
|
||||
elif (regex_no.match(user_input)):
|
||||
return False
|
||||
elif (not ask_until_valid):
|
||||
return None
|
||||
81
cli/parser.py
Normal file
81
cli/parser.py
Normal file
@@ -0,0 +1,81 @@
|
||||
"""Commandline argument parsing utilities."""
|
||||
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
def get_argument_parser():
|
||||
"""Sets up an argparse ArgumentParser and returns it."""
|
||||
|
||||
mainparser = ArgumentParser(
|
||||
description="Debian ISO preseeding tool.",
|
||||
)
|
||||
|
||||
# add mutually exclusive optional arguments to top-level parser
|
||||
mainparser_group = mainparser.add_mutually_exclusive_group()
|
||||
mainparser_group.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
action='store',
|
||||
type=str,
|
||||
dest='path_to_output_file',
|
||||
metavar='OUTPUTFILE',
|
||||
help="File as which the retrieved/generated file will be saved",
|
||||
)
|
||||
mainparser_group.add_argument(
|
||||
"-O",
|
||||
"--output-dir",
|
||||
action='store',
|
||||
type=str,
|
||||
dest='path_to_output_dir',
|
||||
metavar='OUTPUTDIR',
|
||||
help="Directory into which the retrieved/generated file will be written",
|
||||
)
|
||||
|
||||
# register subparsers for the 'get' and 'inject' subcommands
|
||||
subparsers = mainparser.add_subparsers(
|
||||
required=True,
|
||||
title="Subcommands",
|
||||
description="A choice of actions you want udib to take",
|
||||
help="You must specify one of these",
|
||||
dest="subparser_name",
|
||||
)
|
||||
subparser_get = subparsers.add_parser(
|
||||
"get",
|
||||
description="Retrieve an unmodified Debian ISO or example preseed file",
|
||||
)
|
||||
subparser_inject = subparsers.add_parser(
|
||||
"inject",
|
||||
description="Inject a preseed file into a Debian ISO",
|
||||
)
|
||||
|
||||
# register arguments for the 'get' subcommand
|
||||
subparser_get.add_argument(
|
||||
"WHAT",
|
||||
choices=['preseed-file-basic', 'preseed-file-full', 'iso'],
|
||||
action='store',
|
||||
type=str,
|
||||
metavar='WHAT',
|
||||
help="The type of file you want UDIB to retrieve. "
|
||||
"Valid options are: 'preseed-file-basic', 'preseed-file-full' "
|
||||
"or 'iso'.",
|
||||
)
|
||||
|
||||
# register arguments for the 'inject' subcommand
|
||||
subparser_inject.add_argument(
|
||||
"PRESEEDFILE",
|
||||
action='store',
|
||||
type=str,
|
||||
metavar='PRESEEDFILE',
|
||||
help="Path to the preseed file you want to inject",
|
||||
)
|
||||
subparser_inject.add_argument(
|
||||
"-i",
|
||||
"--image-file",
|
||||
action='store',
|
||||
type=str,
|
||||
dest='path_to_image_file',
|
||||
metavar='IMAGEFILE',
|
||||
help="Path to the ISO you want to modify",
|
||||
)
|
||||
|
||||
return mainparser
|
||||
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
239
core/utils.py
Normal file
239
core/utils.py
Normal file
@@ -0,0 +1,239 @@
|
||||
"""A collection of general utilities, not specific to any module."""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
from sys import exit
|
||||
from os import remove, rename
|
||||
from subprocess import run, STDOUT, PIPE
|
||||
|
||||
from cli.clibella import Printer
|
||||
from net.download import download_file
|
||||
from net.scrape import get_debian_iso_urls
|
||||
from gpg.verify import assert_detached_signature_is_valid
|
||||
from gpg.exceptions import VerificationFailedError
|
||||
from gpg.keystore import debian_signing_key_is_imported, import_debian_signing_key
|
||||
from crypt import crypt, METHOD_SHA512
|
||||
from getpass import getpass
|
||||
|
||||
|
||||
def hash_user_password(printer=None):
|
||||
"""Prompts for a password and prints the resulting hash.
|
||||
|
||||
The resulting hash can be added to the preseed file to set a user password:
|
||||
d-i passwd/root-password-crypted PASSWORDHASH
|
||||
"""
|
||||
|
||||
if printer is None:
|
||||
p = Printer()
|
||||
else:
|
||||
if not isinstance(printer, Printer):
|
||||
raise TypeError(f"Expected a {type(Printer)} object.")
|
||||
|
||||
password = getpass("Enter a password: ")
|
||||
password_confirmed = getpass("Enter the password again: ")
|
||||
if not password_confirmed == password:
|
||||
p.failure("Passwords did not match")
|
||||
return
|
||||
|
||||
p.info("Password hash:")
|
||||
p.info(crypt(password, crypt.METHOD_SHA512))
|
||||
|
||||
def find_all_files_under(parent_dir):
|
||||
"""Recursively finds all files anywhere under the specified directory.
|
||||
|
||||
Returns a list of absolute Path objects. Symlinks are ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent_dir : str or pathlike object
|
||||
The directory under which to recursively find files.
|
||||
|
||||
Raises
|
||||
------
|
||||
NotADirectoryError
|
||||
Raised if the specified parent directory is not a directory.
|
||||
|
||||
Examples
|
||||
--------
|
||||
config_files = find_all_files_under("~/.config")
|
||||
|
||||
"""
|
||||
|
||||
if "~" in str(parent_dir):
|
||||
parent_dir = Path(parent_dir).expanduser()
|
||||
parent_dir = Path(parent_dir).resolve()
|
||||
|
||||
if not parent_dir.is_dir():
|
||||
raise NotADirectoryError(f"No such directory: '{parent_dir}'.")
|
||||
|
||||
files = []
|
||||
|
||||
for subpath in parent_dir.iterdir():
|
||||
if subpath.is_file():
|
||||
files.append(subpath.resolve())
|
||||
elif not subpath.is_symlink() and subpath.is_dir():
|
||||
files += find_all_files_under(subpath)
|
||||
|
||||
return files
|
||||
|
||||
|
||||
def trim_text_file(path_to_input_file, substring):
|
||||
"""Removes all lines not containing the substring from the input file.
|
||||
|
||||
The input file is overwritten.
|
||||
If the substring is the empty string, no action is taken.
|
||||
If the substring contains a newline, no action is taken.
|
||||
If the substring does not match any line of the file, the resulting file
|
||||
will be left empty.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_to_input_file : str or pathlike object
|
||||
The file to trim.
|
||||
substring : str
|
||||
The string to filter for.
|
||||
"""
|
||||
|
||||
if not isinstance(substring, str):
|
||||
raise TypeError("Expected a string.")
|
||||
if "\n" in substring or len(substring) == 0:
|
||||
return
|
||||
|
||||
if "~" in str(path_to_input_file):
|
||||
path_to_input_file = Path(path_to_input_file).expanduser()
|
||||
path_to_input_file = Path(path_to_input_file).resolve()
|
||||
|
||||
if not path_to_input_file.is_file():
|
||||
raise FileNotFoundError(f"No such file: '{path_to_input_file}'.")
|
||||
|
||||
# remove unwanted lines from hash file
|
||||
input_file = open(path_to_input_file, "r")
|
||||
input_file_lines = input_file.readlines()
|
||||
input_file_lines_to_keep = []
|
||||
for line in input_file_lines:
|
||||
if substring in line:
|
||||
input_file_lines_to_keep.append(line)
|
||||
input_file.close()
|
||||
remove(path_to_input_file)
|
||||
with open(path_to_input_file, "w") as input_file:
|
||||
input_file.writelines(input_file_lines_to_keep)
|
||||
|
||||
|
||||
def file_is_empty(path_to_input_file):
|
||||
"""Checks whether the input file is empty or not."""
|
||||
|
||||
if "~" in str(path_to_input_file):
|
||||
path_to_input_file = Path(path_to_input_file).expanduser()
|
||||
path_to_input_file = Path(path_to_input_file).resolve()
|
||||
|
||||
if not path_to_input_file.is_file():
|
||||
raise FileNotFoundError(f"No such file: '{path_to_input_file}'.")
|
||||
|
||||
with open(path_to_input_file, 'r') as input_file:
|
||||
# try to read a byte
|
||||
file_has_content = len(input_file.read(1)) > 0
|
||||
|
||||
return not file_has_content
|
||||
|
||||
|
||||
def download_and_verify_debian_iso(path_to_output_file, printer=None):
|
||||
"""Downloads the latest Debian ISO as the specified output file.
|
||||
|
||||
The file's integrity is validated using a SHA512 checksum.
|
||||
The PGP signature of the SHA512SUMS file is checked using gpg.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
path_to_output_file : str or pathlike object
|
||||
Path to the file as which the downloaded image will be saved.
|
||||
printer : clibella.Printer
|
||||
A CLI printer to be used for output.
|
||||
"""
|
||||
|
||||
if "~" in str(path_to_output_file):
|
||||
path_to_output_file = Path(path_to_output_file).expanduser()
|
||||
path_to_output_file = Path(path_to_output_file).resolve()
|
||||
|
||||
if path_to_output_file.is_file():
|
||||
raise FileExistsError(
|
||||
f"Output file '{path_to_output_file}' already exists."
|
||||
)
|
||||
if not path_to_output_file.parent.is_dir():
|
||||
raise NotADirectoryError(
|
||||
f"No such directory: '{path_to_output_file.parent}'."
|
||||
)
|
||||
|
||||
if printer is None:
|
||||
printer = Printer()
|
||||
|
||||
# create a temporary directory
|
||||
with TemporaryDirectory() as temp_dir:
|
||||
# scrape for URLs and filenames
|
||||
files = get_debian_iso_urls()
|
||||
|
||||
# set file paths
|
||||
path_to_hash_file = Path(temp_dir)/files["hash_file"]["name"]
|
||||
path_to_signature_file = Path(temp_dir)/files["signature_file"]["name"]
|
||||
path_to_image_file = Path(temp_dir)/files["image_file"]["name"]
|
||||
|
||||
# download hash file and signature, and verify with gpg
|
||||
download_file(
|
||||
path_to_hash_file,
|
||||
files["hash_file"]["url"],
|
||||
show_progress=False,
|
||||
printer=printer,
|
||||
)
|
||||
download_file(
|
||||
path_to_signature_file,
|
||||
files["signature_file"]["url"],
|
||||
show_progress=False,
|
||||
printer=printer,
|
||||
)
|
||||
|
||||
# verify the hash file using gpg
|
||||
printer.info("Verifying hash file using gpg...")
|
||||
if not debian_signing_key_is_imported():
|
||||
printer.info("Importing Debian public GPG CD signing key...")
|
||||
import_debian_signing_key()
|
||||
else:
|
||||
printer.info("Found Debian public GPG CD signing key.")
|
||||
try:
|
||||
assert_detached_signature_is_valid(
|
||||
path_to_hash_file,
|
||||
path_to_signature_file,
|
||||
)
|
||||
except VerificationFailedError:
|
||||
printer.error("PGP signature of the hash file was invalid!")
|
||||
exit(1)
|
||||
printer.ok("HASH file PGP authenticity check passed.")
|
||||
|
||||
# remove all lines from hash file not containing the image file name
|
||||
trim_text_file(path_to_hash_file, files["image_file"]["name"])
|
||||
if file_is_empty(path_to_hash_file):
|
||||
raise RuntimeError("Failed to locate SHA512 hash sum for image.")
|
||||
|
||||
# download image file
|
||||
download_file(
|
||||
path_to_image_file,
|
||||
files["image_file"]["url"],
|
||||
show_progress=True,
|
||||
printer=printer,
|
||||
)
|
||||
|
||||
# validate SHA512 checksum
|
||||
printer.info("Validating ISO file integrity...")
|
||||
hash_check_result = run(
|
||||
[
|
||||
"sha512sum", "--check", path_to_hash_file
|
||||
],
|
||||
text=True,
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
cwd=path_to_image_file.parent,
|
||||
)
|
||||
if hash_check_result.returncode != 0:
|
||||
raise RuntimeError("SHA512 checksum verification of the ISO failed.")
|
||||
printer.ok("ISO file integrity check passed.")
|
||||
|
||||
# move downloaded file to specified destination
|
||||
rename(path_to_image_file, path_to_output_file)
|
||||
@@ -1,478 +0,0 @@
|
||||
#_preseed_V1
|
||||
|
||||
#### TODOs:
|
||||
# configure partman answers
|
||||
# install sudo and add user to sudo group
|
||||
# configure sshd
|
||||
|
||||
#### Contents of the preconfiguration file (for bullseye)
|
||||
### Localization
|
||||
# Preseeding only locale sets language, country and locale.
|
||||
#d-i debian-installer/locale string en_US
|
||||
|
||||
# The values can also be preseeded individually for greater flexibility.
|
||||
d-i debian-installer/language string en
|
||||
d-i debian-installer/country string DE
|
||||
d-i debian-installer/locale string en_GB.UTF-8
|
||||
# Optionally specify additional locales to be generated.
|
||||
d-i localechooser/supported-locales multiselect en_US.UTF-8, en_DK.UTF-8, de_DE.UTF-8
|
||||
|
||||
# Keyboard selection.
|
||||
d-i keyboard-configuration/xkb-keymap select de
|
||||
# d-i keyboard-configuration/toggle select No toggling
|
||||
|
||||
### Network configuration
|
||||
# Disable network configuration entirely. This is useful for cdrom
|
||||
# installations on non-networked devices where the network questions,
|
||||
# warning and long timeouts are a nuisance.
|
||||
#d-i netcfg/enable boolean false
|
||||
|
||||
# netcfg will choose an interface that has link if possible. This makes it
|
||||
# skip displaying a list if there is more than one interface.
|
||||
d-i netcfg/choose_interface select auto
|
||||
|
||||
# To pick a particular interface instead:
|
||||
#d-i netcfg/choose_interface select eth1
|
||||
|
||||
# To set a different link detection timeout (default is 3 seconds).
|
||||
# Values are interpreted as seconds.
|
||||
#d-i netcfg/link_wait_timeout string 10
|
||||
|
||||
# If you have a slow dhcp server and the installer times out waiting for
|
||||
# it, this might be useful.
|
||||
#d-i netcfg/dhcp_timeout string 60
|
||||
#d-i netcfg/dhcpv6_timeout string 60
|
||||
|
||||
# Automatic network configuration is the default.
|
||||
# If you prefer to configure the network manually, uncomment this line and
|
||||
# the static network configuration below.
|
||||
#d-i netcfg/disable_autoconfig boolean true
|
||||
|
||||
# If you want the preconfiguration file to work on systems both with and
|
||||
# without a dhcp server, uncomment these lines and the static network
|
||||
# configuration below.
|
||||
#d-i netcfg/dhcp_failed note
|
||||
#d-i netcfg/dhcp_options select Configure network manually
|
||||
|
||||
# Static network configuration.
|
||||
#
|
||||
# IPv4 example
|
||||
#d-i netcfg/get_ipaddress string 192.168.1.42
|
||||
#d-i netcfg/get_netmask string 255.255.255.0
|
||||
#d-i netcfg/get_gateway string 192.168.1.1
|
||||
#d-i netcfg/get_nameservers string 192.168.1.1
|
||||
#d-i netcfg/confirm_static boolean true
|
||||
#
|
||||
# IPv6 example
|
||||
#d-i netcfg/get_ipaddress string fc00::2
|
||||
#d-i netcfg/get_netmask string ffff:ffff:ffff:ffff::
|
||||
#d-i netcfg/get_gateway string fc00::1
|
||||
#d-i netcfg/get_nameservers string fc00::1
|
||||
#d-i netcfg/confirm_static boolean true
|
||||
|
||||
# Any hostname and domain names assigned from dhcp take precedence over
|
||||
# values set here. However, setting the values still prevents the questions
|
||||
# from being shown, even if values come from dhcp.
|
||||
d-i netcfg/get_hostname string my-debian-machine
|
||||
d-i netcfg/get_domain string example.com
|
||||
|
||||
# If you want to force a hostname, regardless of what either the DHCP
|
||||
# server returns or what the reverse DNS entry for the IP is, uncomment
|
||||
# and adjust the following line.
|
||||
#d-i netcfg/hostname string somehost
|
||||
|
||||
# Disable that annoying WEP key dialog.
|
||||
d-i netcfg/wireless_wep string
|
||||
# The wacky dhcp hostname that some ISPs use as a password of sorts.
|
||||
#d-i netcfg/dhcp_hostname string radish
|
||||
|
||||
# If non-free firmware is needed for the network or other hardware, you can
|
||||
# configure the installer to always try to load it, without prompting. Or
|
||||
# change to false to disable asking.
|
||||
#d-i hw-detect/load_firmware boolean true
|
||||
|
||||
### Network console
|
||||
# Use the following settings if you wish to make use of the network-console
|
||||
# component for remote installation over SSH. This only makes sense if you
|
||||
# intend to perform the remainder of the installation manually.
|
||||
#d-i anna/choose_modules string network-console
|
||||
#d-i network-console/authorized_keys_url string http://10.0.0.1/openssh-key
|
||||
#d-i network-console/password password r00tme
|
||||
#d-i network-console/password-again password r00tme
|
||||
|
||||
### Mirror settings
|
||||
# Mirror protocol:
|
||||
# If you select ftp, the mirror/country string does not need to be set.
|
||||
# Default value for the mirror protocol: http.
|
||||
#d-i mirror/protocol string ftp
|
||||
d-i mirror/country string manual
|
||||
d-i mirror/http/hostname string debian.tu-bs.de
|
||||
d-i mirror/http/directory string /debian
|
||||
d-i mirror/http/proxy string
|
||||
|
||||
# Suite to install.
|
||||
#d-i mirror/suite string testing
|
||||
# Suite to use for loading installer components (optional).
|
||||
#d-i mirror/udeb/suite string testing
|
||||
|
||||
### Account setup
|
||||
# Skip creation of a root account (normal user account will be able to
|
||||
# use sudo).
|
||||
#d-i passwd/root-login boolean false
|
||||
# Alternatively, to skip creation of a normal user account.
|
||||
#d-i passwd/make-user boolean false
|
||||
|
||||
# Root password, either in clear text
|
||||
#d-i passwd/root-password password r00tme
|
||||
#d-i passwd/root-password-again password r00tme
|
||||
# or encrypted using a crypt(3) hash.
|
||||
# generate using: python3 -c 'import crypt, getpass; print(crypt.crypt(getpass.getpass(), crypt.METHOD_SHA512))'
|
||||
d-i passwd/root-password-crypted password $6$ejIby666HDLKIcgq$SthCbLDIgPlQZb/DNB0XOdgXTYGJIx5cu4qqSAH/KiyFEsDPKWTzyUipqu3TcsMziBo5WxiavPO.a0j8qW2ZY/
|
||||
|
||||
# To create a normal user account.
|
||||
d-i passwd/user-fullname string John Doe
|
||||
d-i passwd/username string administrator
|
||||
# Normal user's password, either in clear text
|
||||
#d-i passwd/user-password password insecure
|
||||
#d-i passwd/user-password-again password insecure
|
||||
# or encrypted using a crypt(3) hash.
|
||||
d-i passwd/user-password-crypted password $6$ejIby666HDLKIcgq$SthCbLDIgPlQZb/DNB0XOdgXTYGJIx5cu4qqSAH/KiyFEsDPKWTzyUipqu3TcsMziBo5WxiavPO.a0j8qW2ZY/
|
||||
# Create the first user with the specified UID instead of the default.
|
||||
#d-i passwd/user-uid string 1010
|
||||
|
||||
# The user account will be added to some standard initial groups. To
|
||||
# override that, use this.
|
||||
#d-i passwd/user-default-groups string audio cdrom video
|
||||
|
||||
### Clock and time zone setup
|
||||
# Controls whether or not the hardware clock is set to UTC.
|
||||
d-i clock-setup/utc boolean true
|
||||
|
||||
# You may set this to any valid setting for $TZ; see the contents of
|
||||
# /usr/share/zoneinfo/ for valid values.
|
||||
d-i time/zone string Europe/Amsterdam
|
||||
|
||||
# Controls whether to use NTP to set the clock during the install
|
||||
d-i clock-setup/ntp boolean true
|
||||
# NTP server to use. The default is almost always fine here.
|
||||
#d-i clock-setup/ntp-server string ntp.example.com
|
||||
|
||||
### Partitioning
|
||||
## Partitioning example
|
||||
# If the system has free space you can choose to only partition that space.
|
||||
# This is only honoured if partman-auto/method (below) is not set.
|
||||
#d-i partman-auto/init_automatically_partition select biggest_free
|
||||
|
||||
# Alternatively, you may specify a disk to partition. If the system has only
|
||||
# one disk the installer will default to using that, but otherwise the device
|
||||
# name must be given in traditional, non-devfs format (so e.g. /dev/sda
|
||||
# and not e.g. /dev/discs/disc0/disc).
|
||||
# For example, to use the first SCSI/SATA hard disk:
|
||||
#d-i partman-auto/disk string /dev/sda
|
||||
# In addition, you'll need to specify the method to use.
|
||||
# The presently available methods are:
|
||||
# - regular: use the usual partition types for your architecture
|
||||
# - lvm: use LVM to partition the disk
|
||||
# - crypto: use LVM within an encrypted partition
|
||||
d-i partman-auto/method string lvm
|
||||
|
||||
# You can define the amount of space that will be used for the LVM volume
|
||||
# group. It can either be a size with its unit (eg. 20 GB), a percentage of
|
||||
# free space or the 'max' keyword.
|
||||
d-i partman-auto-lvm/guided_size string max
|
||||
|
||||
# If one of the disks that are going to be automatically partitioned
|
||||
# contains an old LVM configuration, the user will normally receive a
|
||||
# warning. This can be preseeded away...
|
||||
d-i partman-lvm/device_remove_lvm boolean true
|
||||
# The same applies to pre-existing software RAID array:
|
||||
d-i partman-md/device_remove_md boolean true
|
||||
# And the same goes for the confirmation to write the lvm partitions.
|
||||
d-i partman-lvm/confirm boolean true
|
||||
d-i partman-lvm/confirm_nooverwrite boolean true
|
||||
|
||||
# You can choose one of the three predefined partitioning recipes:
|
||||
# - atomic: all files in one partition
|
||||
# - home: separate /home partition
|
||||
# - multi: separate /home, /var, and /tmp partitions
|
||||
d-i partman-auto/choose_recipe select atomic
|
||||
|
||||
# Or provide a recipe of your own...
|
||||
# If you have a way to get a recipe file into the d-i environment, you can
|
||||
# just point at it.
|
||||
#d-i partman-auto/expert_recipe_file string /hd-media/recipe
|
||||
|
||||
# If not, you can put an entire recipe into the preconfiguration file in one
|
||||
# (logical) line. This example creates a small /boot partition, suitable
|
||||
# swap, and uses the rest of the space for the root partition:
|
||||
#d-i partman-auto/expert_recipe string \
|
||||
# boot-root :: \
|
||||
# 40 50 100 ext3 \
|
||||
# $primary{ } $bootable{ } \
|
||||
# method{ format } format{ } \
|
||||
# use_filesystem{ } filesystem{ ext3 } \
|
||||
# mountpoint{ /boot } \
|
||||
# . \
|
||||
# 500 10000 1000000000 ext3 \
|
||||
# method{ format } format{ } \
|
||||
# use_filesystem{ } filesystem{ ext3 } \
|
||||
# mountpoint{ / } \
|
||||
# . \
|
||||
# 64 512 300% linux-swap \
|
||||
# method{ swap } format{ } \
|
||||
# .
|
||||
|
||||
# The full recipe format is documented in the file partman-auto-recipe.txt
|
||||
# included in the 'debian-installer' package or available from D-I source
|
||||
# repository. This also documents how to specify settings such as file
|
||||
# system labels, volume group names and which physical devices to include
|
||||
# in a volume group.
|
||||
|
||||
## Partitioning for EFI
|
||||
# If your system needs an EFI partition you could add something like
|
||||
# this to the recipe above, as the first element in the recipe:
|
||||
# 538 538 1075 free \
|
||||
# $iflabel{ gpt } \
|
||||
# $reusemethod{ } \
|
||||
# method{ efi } \
|
||||
# format{ } \
|
||||
# . \
|
||||
#
|
||||
# The fragment above is for the amd64 architecture; the details may be
|
||||
# different on other architectures. The 'partman-auto' package in the
|
||||
# D-I source repository may have an example you can follow.
|
||||
|
||||
# This makes partman automatically partition without confirmation, provided
|
||||
# that you told it what to do using one of the methods above.
|
||||
d-i partman-partitioning/confirm_write_new_label boolean true
|
||||
d-i partman/choose_partition select finish
|
||||
d-i partman/confirm boolean true
|
||||
d-i partman/confirm_nooverwrite boolean true
|
||||
|
||||
# Force UEFI booting ('BIOS compatibility' will be lost). Default: false.
|
||||
#d-i partman-efi/non_efi_system boolean true
|
||||
# Ensure the partition table is GPT - this is required for EFI
|
||||
#d-i partman-partitioning/choose_label select gpt
|
||||
#d-i partman-partitioning/default_label string gpt
|
||||
|
||||
# When disk encryption is enabled, skip wiping the partitions beforehand.
|
||||
#d-i partman-auto-crypto/erase_disks boolean false
|
||||
|
||||
## Partitioning using RAID
|
||||
# The method should be set to "raid".
|
||||
#d-i partman-auto/method string raid
|
||||
# Specify the disks to be partitioned. They will all get the same layout,
|
||||
# so this will only work if the disks are the same size.
|
||||
#d-i partman-auto/disk string /dev/sda /dev/sdb
|
||||
|
||||
# Next you need to specify the physical partitions that will be used.
|
||||
#d-i partman-auto/expert_recipe string \
|
||||
# multiraid :: \
|
||||
# 1000 5000 4000 raid \
|
||||
# $primary{ } method{ raid } \
|
||||
# . \
|
||||
# 64 512 300% raid \
|
||||
# method{ raid } \
|
||||
# . \
|
||||
# 500 10000 1000000000 raid \
|
||||
# method{ raid } \
|
||||
# .
|
||||
|
||||
# Last you need to specify how the previously defined partitions will be
|
||||
# used in the RAID setup. Remember to use the correct partition numbers
|
||||
# for logical partitions. RAID levels 0, 1, 5, 6 and 10 are supported;
|
||||
# devices are separated using "#".
|
||||
# Parameters are:
|
||||
# <raidtype> <devcount> <sparecount> <fstype> <mountpoint> \
|
||||
# <devices> <sparedevices>
|
||||
|
||||
#d-i partman-auto-raid/recipe string \
|
||||
# 1 2 0 ext3 / \
|
||||
# /dev/sda1#/dev/sdb1 \
|
||||
# . \
|
||||
# 1 2 0 swap - \
|
||||
# /dev/sda5#/dev/sdb5 \
|
||||
# . \
|
||||
# 0 2 0 ext3 /home \
|
||||
# /dev/sda6#/dev/sdb6 \
|
||||
# .
|
||||
|
||||
# For additional information see the file partman-auto-raid-recipe.txt
|
||||
# included in the 'debian-installer' package or available from D-I source
|
||||
# repository.
|
||||
|
||||
# This makes partman automatically partition without confirmation.
|
||||
d-i partman-md/confirm boolean true
|
||||
d-i partman-partitioning/confirm_write_new_label boolean true
|
||||
d-i partman/choose_partition select finish
|
||||
d-i partman/confirm boolean true
|
||||
d-i partman/confirm_nooverwrite boolean true
|
||||
|
||||
## Controlling how partitions are mounted
|
||||
# The default is to mount by UUID, but you can also choose "traditional" to
|
||||
# use traditional device names, or "label" to try filesystem labels before
|
||||
# falling back to UUIDs.
|
||||
#d-i partman/mount_style select uuid
|
||||
|
||||
### Base system installation
|
||||
# Configure APT to not install recommended packages by default. Use of this
|
||||
# option can result in an incomplete system and should only be used by very
|
||||
# experienced users.
|
||||
#d-i base-installer/install-recommends boolean false
|
||||
|
||||
# The kernel image (meta) package to be installed; "none" can be used if no
|
||||
# kernel is to be installed.
|
||||
#d-i base-installer/kernel/image string linux-image-686
|
||||
|
||||
### Apt setup
|
||||
# Choose, if you want to scan additional installation media
|
||||
# (default: false).
|
||||
d-i apt-setup/cdrom/set-first boolean false
|
||||
# You can choose to install non-free and contrib software.
|
||||
#d-i apt-setup/non-free boolean true
|
||||
#d-i apt-setup/contrib boolean true
|
||||
# Uncomment the following line, if you don't want to have the sources.list
|
||||
# entry for a DVD/BD installation image active in the installed system
|
||||
# (entries for netinst or CD images will be disabled anyway, regardless of
|
||||
# this setting).
|
||||
#d-i apt-setup/disable-cdrom-entries boolean true
|
||||
# Uncomment this if you don't want to use a network mirror.
|
||||
#d-i apt-setup/use_mirror boolean false
|
||||
# Select which update services to use; define the mirrors to be used.
|
||||
# Values shown below are the normal defaults.
|
||||
#d-i apt-setup/services-select multiselect security, updates
|
||||
#d-i apt-setup/security_host string security.debian.org
|
||||
|
||||
# Additional repositories, local[0-9] available
|
||||
#d-i apt-setup/local0/repository string \
|
||||
# http://local.server/debian stable main
|
||||
#d-i apt-setup/local0/comment string local server
|
||||
# Enable deb-src lines
|
||||
#d-i apt-setup/local0/source boolean true
|
||||
# URL to the public key of the local repository; you must provide a key or
|
||||
# apt will complain about the unauthenticated repository and so the
|
||||
# sources.list line will be left commented out.
|
||||
#d-i apt-setup/local0/key string http://local.server/key
|
||||
# If the provided key file ends in ".asc" the key file needs to be an
|
||||
# ASCII-armoured PGP key, if it ends in ".gpg" it needs to use the
|
||||
# "GPG key public keyring" format, the "keybox database" format is
|
||||
# currently not supported.
|
||||
|
||||
# By default the installer requires that repositories be authenticated
|
||||
# using a known gpg key. This setting can be used to disable that
|
||||
# authentication. Warning: Insecure, not recommended.
|
||||
#d-i debian-installer/allow_unauthenticated boolean true
|
||||
|
||||
# Uncomment this to add multiarch configuration for i386
|
||||
#d-i apt-setup/multiarch string i386
|
||||
|
||||
|
||||
### Package selection
|
||||
tasksel tasksel/first multiselect standard, ssh-server
|
||||
|
||||
# Or choose to not get the tasksel dialog displayed at all (and don't install
|
||||
# any packages):
|
||||
#d-i pkgsel/run_tasksel boolean false
|
||||
|
||||
# Individual additional packages to install
|
||||
#d-i pkgsel/include string openssh-server build-essential
|
||||
# Whether to upgrade packages after debootstrap.
|
||||
# Allowed values: none, safe-upgrade, full-upgrade
|
||||
d-i pkgsel/upgrade select full-upgrade
|
||||
|
||||
# You can choose, if your system will report back on what software you have
|
||||
# installed, and what software you use. The default is not to report back,
|
||||
# but sending reports helps the project determine what software is most
|
||||
# popular and should be included on the first CD/DVD.
|
||||
popularity-contest popularity-contest/participate boolean false
|
||||
|
||||
### Boot loader installation
|
||||
# Grub is the boot loader (for x86).
|
||||
|
||||
# This is fairly safe to set, it makes grub install automatically to the UEFI
|
||||
# partition/boot record if no other operating system is detected on the machine.
|
||||
d-i grub-installer/only_debian boolean true
|
||||
|
||||
# This one makes grub-installer install to the UEFI partition/boot record, if
|
||||
# it also finds some other OS, which is less safe as it might not be able to
|
||||
# boot that other OS.
|
||||
d-i grub-installer/with_other_os boolean true
|
||||
|
||||
# Due notably to potential USB sticks, the location of the primary drive can
|
||||
# not be determined safely in general, so this needs to be specified:
|
||||
d-i grub-installer/bootdev string /dev/sda
|
||||
# To install to the primary device (assuming it is not a USB stick):
|
||||
#d-i grub-installer/bootdev string default
|
||||
|
||||
# Alternatively, if you want to install to a location other than the UEFI
|
||||
# parition/boot record, uncomment and edit these lines:
|
||||
#d-i grub-installer/only_debian boolean false
|
||||
#d-i grub-installer/with_other_os boolean false
|
||||
#d-i grub-installer/bootdev string (hd0,1)
|
||||
# To install grub to multiple disks:
|
||||
#d-i grub-installer/bootdev string (hd0,1) (hd1,1) (hd2,1)
|
||||
|
||||
# Optional password for grub, either in clear text
|
||||
#d-i grub-installer/password password r00tme
|
||||
#d-i grub-installer/password-again password r00tme
|
||||
# or encrypted using an MD5 hash, see grub-md5-crypt(8).
|
||||
#d-i grub-installer/password-crypted password [MD5 hash]
|
||||
|
||||
# Use the following option to add additional boot parameters for the
|
||||
# installed system (if supported by the bootloader installer).
|
||||
# Note: options passed to the installer will be added automatically.
|
||||
#d-i debian-installer/add-kernel-opts string nousb
|
||||
|
||||
### Finishing up the installation
|
||||
# During installations from serial console, the regular virtual consoles
|
||||
# (VT1-VT6) are normally disabled in /etc/inittab. Uncomment the next
|
||||
# line to prevent this.
|
||||
#d-i finish-install/keep-consoles boolean true
|
||||
|
||||
# Avoid that last message about the install being complete.
|
||||
d-i finish-install/reboot_in_progress note
|
||||
|
||||
# This will prevent the installer from ejecting the CD during the reboot,
|
||||
# which is useful in some situations.
|
||||
#d-i cdrom-detect/eject boolean false
|
||||
|
||||
# This is how to make the installer shutdown when finished, but not
|
||||
# reboot into the installed system.
|
||||
#d-i debian-installer/exit/halt boolean true
|
||||
# This will power off the machine instead of just halting it.
|
||||
#d-i debian-installer/exit/poweroff boolean true
|
||||
|
||||
### Preseeding other packages
|
||||
# Depending on what software you choose to install, or if things go wrong
|
||||
# during the installation process, it's possible that other questions may
|
||||
# be asked. You can preseed those too, of course. To get a list of every
|
||||
# possible question that could be asked during an install, do an
|
||||
# installation, and then run these commands:
|
||||
# debconf-get-selections --installer > file
|
||||
# debconf-get-selections >> file
|
||||
|
||||
|
||||
#### Advanced options
|
||||
### Running custom commands during the installation
|
||||
# d-i preseeding is inherently not secure. Nothing in the installer checks
|
||||
# for attempts at buffer overflows or other exploits of the values of a
|
||||
# preconfiguration file like this one. Only use preconfiguration files from
|
||||
# trusted locations! To drive that home, and because it's generally useful,
|
||||
# here's a way to run any shell command you'd like inside the installer,
|
||||
# automatically.
|
||||
|
||||
# This first command is run as early as possible, just after
|
||||
# preseeding is read.
|
||||
#d-i preseed/early_command string anna-install some-udeb
|
||||
# This command is run immediately before the partitioner starts. It may be
|
||||
# useful to apply dynamic partitioner preseeding that depends on the state
|
||||
# of the disks (which may not be visible when preseed/early_command runs).
|
||||
#d-i partman/early_command \
|
||||
# string debconf-set partman-auto/disk "$(list-devices disk | head -n1)"
|
||||
# This command is run just before the install finishes, but when there is
|
||||
# still a usable /target directory. You can chroot to /target and use it
|
||||
# directly, or use the apt-install and in-target commands to easily install
|
||||
# packages and run commands in the target system.
|
||||
#d-i preseed/late_command string apt-install zsh; in-target chsh -s /bin/zsh
|
||||
|
||||
|
||||
0
gpg/__init__.py
Normal file
0
gpg/__init__.py
Normal file
37
gpg/exceptions.py
Normal file
37
gpg/exceptions.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""Exceptions which may be raised during execution of gpg wrapper functions."""
|
||||
|
||||
|
||||
class GpgProgrammingException(Exception):
|
||||
"""A general exception indicating a programming error."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class GpgRuntimeError(RuntimeError):
|
||||
"""A general exception indicating a runtime error."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class UnexpectedOutputException(GpgProgrammingException):
|
||||
"""Raised when a gpg subprocess produces unexpected output."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class MissingLocalKeyError(GpgRuntimeError):
|
||||
"""Raised when an expected key is missing from the local keystore."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSignatureError(GpgRuntimeError):
|
||||
"""Raised when an invalid gpg signature is encountered."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VerificationFailedError(GpgRuntimeError):
|
||||
"""Raised when a gpg verfication encounters a bad signature."""
|
||||
|
||||
pass
|
||||
112
gpg/keystore.py
Normal file
112
gpg/keystore.py
Normal file
@@ -0,0 +1,112 @@
|
||||
"""Utilities for searching and importing GPG keys locally."""
|
||||
|
||||
from subprocess import run, STDOUT, PIPE
|
||||
from re import compile
|
||||
|
||||
|
||||
_DEBIAN_KEY_SERVER_HOSTNAME = "keyring.debian.org"
|
||||
_DEBIAN_CD_SIGNING_KEY_ID = "DA87E80D6294BE9B"
|
||||
|
||||
|
||||
def import_debian_signing_key():
|
||||
"""Imports the public debian CD signing key using gpg.
|
||||
|
||||
The key is imported from keyring.debian.org into the invoking user's
|
||||
GPG public key store using a shell command.
|
||||
"""
|
||||
|
||||
# execute a gpg key import as a shell command, redirecting stderr to stdout
|
||||
process_result = run(
|
||||
[
|
||||
"gpg", "--keyserver", _DEBIAN_KEY_SERVER_HOSTNAME,
|
||||
"--recv-key", _DEBIAN_CD_SIGNING_KEY_ID
|
||||
],
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# check shell return code
|
||||
if process_result.returncode != 0:
|
||||
if process_result.stdout:
|
||||
raise RuntimeError(
|
||||
f"Failed to import key using gpg:\n{process_result.stdout}"
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("Failed to import key using gpg.")
|
||||
|
||||
# check shell output:
|
||||
# the first line of stdout should look like this
|
||||
expected_first_line = str(
|
||||
f"gpg: key {_DEBIAN_CD_SIGNING_KEY_ID}: public key "
|
||||
f"\"Debian CD signing key <debian-cd@lists.debian.org>\""
|
||||
f" imported"
|
||||
)
|
||||
|
||||
if not process_result.stdout.split("\n")[0] == expected_first_line:
|
||||
raise RuntimeError(
|
||||
f"Unexpected output while importing PGP public key:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
|
||||
|
||||
def debian_signing_key_is_imported():
|
||||
"""Checks whether the debian PGP signing key exists in the local key store.
|
||||
|
||||
The invoking user's GPG key store is checked using a shell command.
|
||||
|
||||
Returns
|
||||
-------
|
||||
True : bool
|
||||
If the public PGP debian cd signing key exists in the invoking user's
|
||||
GPG key store.
|
||||
False : bool
|
||||
If the public PGP debian cd signing key does not exist in the invoking
|
||||
user's GPG key store.
|
||||
"""
|
||||
|
||||
# execute a local gpg key lookup as a shell command, redirecting stderr to
|
||||
# stdout
|
||||
# NOTE: this command returns 0 even if the key is not present
|
||||
process_result = run(
|
||||
["gpg", "--locate-keys", _DEBIAN_CD_SIGNING_KEY_ID],
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
text=True,
|
||||
)
|
||||
|
||||
# check shell return code
|
||||
if process_result.returncode != 0:
|
||||
raise RuntimeError("Failed to search local keys using gpg.")
|
||||
|
||||
# no shell output means that the key does not exist locally
|
||||
if not process_result.stdout:
|
||||
return False
|
||||
|
||||
# verify existing key shell output using regex:
|
||||
# it should contain six lines in the following format
|
||||
expected_output_lines_regexes = [
|
||||
compile(r"^pub .*$"),
|
||||
compile(r"^ *[0-9A-F]{40}$"),
|
||||
compile(r"^uid .*$"),
|
||||
compile(r"^sub .*$"),
|
||||
compile(r"^$"),
|
||||
compile(r"^$"),
|
||||
]
|
||||
|
||||
actual_output_lines = process_result.stdout.split("\n")
|
||||
if not len(actual_output_lines) == len(expected_output_lines_regexes):
|
||||
raise RuntimeError(
|
||||
f"Unexpected line count in shell output while performing local"
|
||||
f"GPG key lookup:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
for i in range(4):
|
||||
if not expected_output_lines_regexes[i].match(actual_output_lines[i]):
|
||||
raise RuntimeError(
|
||||
f"Unexpected shell output format while performing local"
|
||||
f"GPG key lookup:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
|
||||
return True
|
||||
128
gpg/verify.py
Normal file
128
gpg/verify.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Utilities for verifying files using gpg."""
|
||||
|
||||
from re import compile
|
||||
from pathlib import Path
|
||||
from subprocess import run, PIPE, STDOUT
|
||||
|
||||
import gpg.exceptions as ex
|
||||
|
||||
|
||||
def assert_detached_signature_is_valid(
|
||||
path_to_input_file,
|
||||
path_to_signature_file
|
||||
):
|
||||
"""Verifies the input file using the specified detached gpg signature.
|
||||
|
||||
The invoking user's local gpg key store is used for verification, the
|
||||
public key used to create the signature must be present in the invoking
|
||||
user's local gpg key store.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_to_input_file : str or pathlike object
|
||||
Path to the file which should be verified.
|
||||
path_to_signature_file : str or pathlike object
|
||||
Path to the file containing a detached gpg signature of the input file.
|
||||
|
||||
Raises
|
||||
------
|
||||
FileNotFoundError
|
||||
If either of the input files do not exist.
|
||||
gpg.exceptions.MissingLocalKeyError
|
||||
If a key referenced by the signature file could not be found in the
|
||||
invoking user's local key store.
|
||||
gpg.exceptions.VerificationFailedError
|
||||
If the gpg verification detects a bad signature.
|
||||
"""
|
||||
|
||||
if '~' in str(path_to_input_file):
|
||||
path_to_input_file = Path(path_to_input_file).expanduser()
|
||||
path_to_input_file = Path(path_to_input_file).resolve()
|
||||
|
||||
if '~' in str(path_to_signature_file):
|
||||
path_to_signature_file = Path(path_to_signature_file).expanduser()
|
||||
path_to_signature_file = Path(path_to_signature_file).resolve()
|
||||
|
||||
if not path_to_input_file.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"No such file: '{path_to_input_file}'."
|
||||
)
|
||||
if not path_to_signature_file.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"No such file: '{path_to_signature_file}'."
|
||||
)
|
||||
|
||||
# execute a gpg verification as a shell command, redirecting stderr to
|
||||
# stdout
|
||||
process_result = run(
|
||||
[
|
||||
"gpg", "--verify", path_to_signature_file, path_to_input_file,
|
||||
],
|
||||
stdout=PIPE,
|
||||
stderr=STDOUT,
|
||||
text=True,
|
||||
)
|
||||
output_lines = process_result.stdout.split("\n")
|
||||
if len(output_lines) < 3:
|
||||
raise ex.UnexpectedOutputException(
|
||||
f"Unexpected output during gpg verification:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
|
||||
if process_result.returncode == 2:
|
||||
# a missing local key causes return code 2
|
||||
# and the following output on the third line:
|
||||
missing_key_regex = compile(
|
||||
r"^gpg: Can't check signature: No public key$"
|
||||
)
|
||||
# an invalid detached signature file causes return code 2
|
||||
# and the following output on the first line:
|
||||
invalid_signature_regex = compile(
|
||||
r"^gpg: no valid OpenPGP data found.$"
|
||||
)
|
||||
|
||||
if missing_key_regex.match(output_lines[2]):
|
||||
raise ex.MissingLocalKeyError(
|
||||
"Failed to verify gpg signature: no matching local key."
|
||||
)
|
||||
elif invalid_signature_regex.match(output_lines[0]):
|
||||
raise ex.InvalidSignatureError(
|
||||
"Invalid signature file."
|
||||
)
|
||||
else:
|
||||
raise ex.UnexpectedOutputException(
|
||||
f"Unexpected output during gpg verification:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
elif process_result.returncode == 1:
|
||||
# failed verification causes return code 1
|
||||
# and the following output on line 3:
|
||||
verification_failed_regex = compile(
|
||||
r"^gpg: BAD signature from .*$"
|
||||
)
|
||||
if not verification_failed_regex.match(output_lines[2]):
|
||||
raise ex.UnexpectedOutputException(
|
||||
f"Unexpected output during gpg verification:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
else:
|
||||
raise ex.VerificationFailedError(
|
||||
"gpg signature verification failed: BAD SIGNATURE!"
|
||||
)
|
||||
elif process_result.returncode == 0:
|
||||
# successful verification causes return code 0
|
||||
# and the following output on line 3:
|
||||
verification_successful_regex = compile(
|
||||
r"^gpg: Good signature from .*$"
|
||||
)
|
||||
if not verification_successful_regex.match(output_lines[2]):
|
||||
raise ex.UnexpectedOutputException(
|
||||
f"Unexpected output during gpg verification:\n"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
else:
|
||||
raise ex.UnexpectedOutputException(
|
||||
f"Unexpected return code during gpg verification:\n"
|
||||
f"{process_result.returncode}"
|
||||
f"{process_result.stdout}"
|
||||
)
|
||||
0
iso/__init__.py
Normal file
0
iso/__init__.py
Normal file
@@ -1,4 +1,4 @@
|
||||
"""Library for modification of disk image files.
|
||||
"""Utilities 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
|
||||
@@ -8,48 +8,15 @@ local filesystem.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
import gzip
|
||||
import hashlib
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
|
||||
def _find_all_files_under(parent_dir):
|
||||
"""Recursively finds all files anywhere under the specified directory.
|
||||
|
||||
Returns a list of absolute Path objects. Symlinks are ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
parent_dir : str or pathlike object
|
||||
The directory under which to recursively find files.
|
||||
|
||||
Raises
|
||||
------
|
||||
NotADirectoryError
|
||||
Raised if the specified parent directory is not a directory.
|
||||
|
||||
Examples
|
||||
--------
|
||||
config_files = _find_all_files_under("~/.config")
|
||||
|
||||
"""
|
||||
|
||||
parent_dir = Path(parent_dir).resolve()
|
||||
|
||||
if not parent_dir.is_dir():
|
||||
raise NotADirectoryError(f"No such directory: '{parent_dir}'.")
|
||||
|
||||
files = []
|
||||
|
||||
for subpath in parent_dir.iterdir():
|
||||
if subpath.is_file():
|
||||
files.append(subpath.resolve())
|
||||
elif not subpath.is_symlink() and subpath.is_dir():
|
||||
files += _find_all_files_under(subpath)
|
||||
|
||||
return files
|
||||
from cli.clibella import Printer
|
||||
from core.utils import find_all_files_under
|
||||
|
||||
|
||||
def extract_iso(path_to_output_dir, path_to_input_file):
|
||||
@@ -78,8 +45,13 @@ def extract_iso(path_to_output_dir, path_to_input_file):
|
||||
|
||||
"""
|
||||
|
||||
path_to_output_dir = Path(path_to_output_dir)
|
||||
path_to_input_file = Path(path_to_input_file)
|
||||
if "~" in str(path_to_output_dir):
|
||||
path_to_output_dir = Path(path_to_output_dir).expanduser()
|
||||
path_to_output_dir = Path(path_to_output_dir).resolve()
|
||||
|
||||
if "~" in str(path_to_input_file):
|
||||
path_to_input_file = Path(path_to_input_file).expanduser()
|
||||
path_to_input_file = Path(path_to_input_file).resolve()
|
||||
|
||||
# check if paths are valid
|
||||
if not path_to_output_dir.is_dir():
|
||||
@@ -93,19 +65,23 @@ def extract_iso(path_to_output_dir, path_to_input_file):
|
||||
[
|
||||
"xorriso",
|
||||
"-osirrox", "on",
|
||||
"-indev", path_to_input_file.resolve(),
|
||||
"-indev", path_to_input_file,
|
||||
"-extract", "/",
|
||||
path_to_output_dir.resolve()
|
||||
path_to_output_dir
|
||||
],
|
||||
capture_output=True,
|
||||
check=True)
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
raise RuntimeError(
|
||||
f"An error occurred while extracting '{path_to_input_file}'.")
|
||||
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):
|
||||
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
|
||||
@@ -137,8 +113,13 @@ def append_file_contents_to_initrd_archive(path_to_initrd_archive,
|
||||
|
||||
"""
|
||||
|
||||
path_to_initrd_archive = Path(path_to_initrd_archive)
|
||||
path_to_input_file = Path(path_to_input_file)
|
||||
if "~" in str(path_to_initrd_archive):
|
||||
path_to_initrd_archive = Path(path_to_initrd_archive).expanduser()
|
||||
path_to_initrd_archive = Path(path_to_initrd_archive).resolve()
|
||||
|
||||
if "~" in str(path_to_input_file):
|
||||
path_to_input_file = Path(path_to_input_file).expanduser()
|
||||
path_to_input_file = Path(path_to_input_file).resolve()
|
||||
|
||||
# check if initrd file exists and has the correct name
|
||||
if not path_to_initrd_archive.is_file():
|
||||
@@ -218,12 +199,15 @@ def regenerate_iso_md5sums_file(path_to_extracted_iso_root):
|
||||
|
||||
"""
|
||||
|
||||
if "~" in str(path_to_extracted_iso_root):
|
||||
path_to_extracted_iso_root = Path(path_to_extracted_iso_root).expanduser()
|
||||
path_to_extracted_iso_root = Path(path_to_extracted_iso_root).resolve()
|
||||
|
||||
# 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}'.")
|
||||
raise NotADirectoryError(
|
||||
f"No such directory: '{path_to_extracted_iso_root}'."
|
||||
)
|
||||
|
||||
path_to_md5sum_file = path_to_extracted_iso_root/"md5sum.txt"
|
||||
|
||||
@@ -240,7 +224,7 @@ def regenerate_iso_md5sums_file(path_to_extracted_iso_root):
|
||||
# Note the two spaces between hash and filepath!
|
||||
|
||||
# find all files
|
||||
subpaths = _find_all_files_under(path_to_extracted_iso_root)
|
||||
subpaths = find_all_files_under(path_to_extracted_iso_root)
|
||||
|
||||
with open(path_to_md5sum_file, "w") as md5sum_file:
|
||||
for subpath in subpaths:
|
||||
@@ -252,7 +236,8 @@ def regenerate_iso_md5sums_file(path_to_extracted_iso_root):
|
||||
md5hash.hexdigest()
|
||||
+ " "
|
||||
+ str(subpath.relative_to(path_to_extracted_iso_root))
|
||||
+ "\n")
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
# revert write permissions from md5sum.txt and its parent dir
|
||||
path_to_md5sum_file.chmod(0o444)
|
||||
@@ -291,7 +276,12 @@ def extract_mbr_from_iso(path_to_output_file, path_to_source_iso):
|
||||
|
||||
"""
|
||||
|
||||
path_to_output_file = Path(path_to_output_file)
|
||||
if "~" in str(path_to_output_file):
|
||||
path_to_output_file = Path(path_to_output_file).expanduser()
|
||||
path_to_output_file = Path(path_to_output_file).resolve()
|
||||
|
||||
if "~" in str(path_to_source_iso):
|
||||
path_to_source_iso = Path(path_to_source_iso).expanduser()
|
||||
path_to_source_iso = Path(path_to_source_iso)
|
||||
|
||||
# make sure output file does not exist already
|
||||
@@ -363,9 +353,17 @@ def repack_iso(path_to_output_iso,
|
||||
|
||||
"""
|
||||
|
||||
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)
|
||||
if "~" in str(path_to_output_iso):
|
||||
path_to_output_iso = Path(path_to_output_iso).expanduser()
|
||||
path_to_output_iso = Path(path_to_output_iso).resolve()
|
||||
|
||||
if "~" in str(path_to_mbr_data_file):
|
||||
path_to_mbr_data_file = Path(path_to_mbr_data_file).expanduser()
|
||||
path_to_mbr_data_file = Path(path_to_mbr_data_file).resolve()
|
||||
|
||||
if "~" in str(path_to_input_files_root_dir):
|
||||
path_to_input_files_root_dir = Path(path_to_input_files_root_dir).expanduser()
|
||||
path_to_input_files_root_dir = Path(path_to_input_files_root_dir).resolve()
|
||||
|
||||
# make sure output file does not exist yet
|
||||
if path_to_output_iso.exists():
|
||||
@@ -391,21 +389,136 @@ def repack_iso(path_to_output_iso,
|
||||
# repack the ISO using xorriso
|
||||
try:
|
||||
subprocess.run(
|
||||
["xorriso", "-as", "mkisofs",
|
||||
[
|
||||
"xorriso", "-as", "mkisofs",
|
||||
"-r", "-V", created_iso_filesystem_name,
|
||||
"-o", path_to_output_iso.resolve(),
|
||||
"-o", path_to_output_iso,
|
||||
"-J", "-J", "-joliet-long", "-cache-inodes",
|
||||
"-isohybrid-mbr", path_to_mbr_data_file.resolve(),
|
||||
"-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.resolve()],
|
||||
path_to_input_files_root_dir,
|
||||
],
|
||||
capture_output=True,
|
||||
check=True)
|
||||
check=True,
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError:
|
||||
raise RuntimeError(f"Failed while repacking ISO from source files: "
|
||||
f"'{path_to_input_files_root_dir}'.")
|
||||
|
||||
|
||||
def inject_preseed_file_into_iso(
|
||||
path_to_output_iso_file,
|
||||
path_to_input_iso_file,
|
||||
path_to_input_preseed_file,
|
||||
iso_filesystem_name="Debian",
|
||||
printer=None,
|
||||
):
|
||||
"""Injects the specified preseed file 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
|
||||
extracted ISO's initrd, then regenerates the extracted ISO's internal MD5
|
||||
hash list and finally repacks the extracted ISO into the output ISO.
|
||||
|
||||
The input ISO file itself is left unchanged.
|
||||
The output ISO file is newly created.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_to_output_iso_file : str or pathlike object
|
||||
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.
|
||||
printer : clibella.Printer
|
||||
A printer for CLI output.
|
||||
"""
|
||||
|
||||
# verify and resolve paths
|
||||
if "~" in str(path_to_input_iso_file):
|
||||
path_to_input_iso_file = Path(path_to_input_iso_file).expanduser()
|
||||
path_to_input_iso_file = Path(path_to_input_iso_file).resolve()
|
||||
if not path_to_input_iso_file.is_file():
|
||||
raise FileNotFoundError(f"No such file: '{path_to_input_iso_file}'.")
|
||||
|
||||
if "~" in str(path_to_output_iso_file):
|
||||
path_to_output_iso_file = Path(path_to_output_iso_file).expanduser()
|
||||
path_to_output_iso_file = Path(path_to_output_iso_file).resolve()
|
||||
if path_to_output_iso_file.is_file():
|
||||
raise FileExistsError(f"Output file exists: '{path_to_input_iso_file}'.")
|
||||
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}'.")
|
||||
|
||||
if printer is None:
|
||||
p = Printer()
|
||||
else:
|
||||
if not isinstance(printer, Printer):
|
||||
raise TypeError(f"Expected a {type(Printer)} object.")
|
||||
p = printer
|
||||
|
||||
# extract image file to a temporary directory
|
||||
temp_extracted_iso_dir = TemporaryDirectory()
|
||||
path_to_extracted_iso_dir = Path(temp_extracted_iso_dir.name)
|
||||
p.info(f"Extracting contents of {path_to_input_iso_file.name}...")
|
||||
extract_iso(
|
||||
path_to_extracted_iso_dir,
|
||||
path_to_input_iso_file
|
||||
)
|
||||
p.ok("ISO extraction complete.")
|
||||
|
||||
# extract ISO MBR into a temporary directory
|
||||
p.info(f"Extracting MBR from {path_to_input_iso_file.name}...")
|
||||
temp_mbr_dir = TemporaryDirectory()
|
||||
path_to_mbr_dir = Path(temp_mbr_dir.name)
|
||||
path_to_mbr_file = path_to_mbr_dir/"mbr.bin"
|
||||
extract_mbr_from_iso(
|
||||
path_to_mbr_file,
|
||||
path_to_input_iso_file,
|
||||
)
|
||||
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.")
|
||||
|
||||
# regenerate extracted ISO's md5sum.txt file
|
||||
p.info("Regenerating MD5 checksums...")
|
||||
regenerate_iso_md5sums_file(path_to_extracted_iso_dir)
|
||||
p.ok("MD5 calculations complete.")
|
||||
|
||||
# repack exctracted ISO into a single file
|
||||
p.info("Repacking ISO...")
|
||||
repack_iso(
|
||||
path_to_output_iso_file,
|
||||
path_to_mbr_file,
|
||||
path_to_extracted_iso_dir,
|
||||
iso_filesystem_name
|
||||
)
|
||||
p.success(f"ISO file was created successfully at '{path_to_output_iso_file}'.")
|
||||
|
||||
# clear out temporary directories
|
||||
temp_preseed_dir.cleanup()
|
||||
temp_mbr_dir.cleanup()
|
||||
temp_extracted_iso_dir.cleanup()
|
||||
0
net/__init__.py
Normal file
0
net/__init__.py
Normal file
79
net/download.py
Normal file
79
net/download.py
Normal file
@@ -0,0 +1,79 @@
|
||||
"""Library for downloading files from the web with CLI output."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from tqdm import tqdm
|
||||
|
||||
from cli.clibella import Printer
|
||||
|
||||
|
||||
def download_file(
|
||||
path_to_output_file,
|
||||
url_to_file,
|
||||
show_progress=False,
|
||||
printer=None,
|
||||
):
|
||||
"""Downloads the file at the input URL to the specified path.
|
||||
|
||||
The file is downloaded via HTTP/HTTPS and saved to the specified path.
|
||||
Optionally, displays a nice status bar.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_to_output_file : str or pathlike object
|
||||
Path to a file as which the downloaded file is saved.
|
||||
url_to_file : str
|
||||
URL to the file to be downloaded.
|
||||
show_progress : bool
|
||||
When True, a progress bar is displayed on StdOut indicating the
|
||||
progress of the download.
|
||||
printer : clibella.Printer
|
||||
A clibella.Printer used to print CLI output.
|
||||
"""
|
||||
|
||||
if '~' in str(path_to_output_file):
|
||||
path_to_output_file = Path(path_to_output_file).expanduser()
|
||||
path_to_output_file = Path(path_to_output_file).resolve()
|
||||
|
||||
if not path_to_output_file.parent.is_dir():
|
||||
raise FileNotFoundError(
|
||||
f"No such directory: '{path_to_output_file.parent}'."
|
||||
)
|
||||
if path_to_output_file.exists():
|
||||
raise FileExistsError(
|
||||
f"File already exists: '{path_to_output_file}'"
|
||||
)
|
||||
|
||||
if printer is None:
|
||||
p = Printer()
|
||||
else:
|
||||
p = printer
|
||||
|
||||
output_file_name = path_to_output_file.name
|
||||
with open(path_to_output_file, "wb") as output_file:
|
||||
p.info(f"Downloading '{output_file_name}'...")
|
||||
file_response = requests.get(url_to_file, stream=True)
|
||||
total_length = file_response.headers.get('content-length')
|
||||
|
||||
if total_length is None: # no content length header
|
||||
output_file.write(file_response.content)
|
||||
else:
|
||||
if (show_progress):
|
||||
total_length = int(total_length)
|
||||
progress_bar = tqdm(
|
||||
total=total_length,
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024
|
||||
)
|
||||
|
||||
for data in file_response.iter_content(chunk_size=4096):
|
||||
output_file.write(data)
|
||||
if (show_progress):
|
||||
progress_bar.update(len(data))
|
||||
|
||||
if (show_progress):
|
||||
progress_bar.close()
|
||||
|
||||
p.ok(f"Received '{output_file_name}'.")
|
||||
108
net/scrape.py
Normal file
108
net/scrape.py
Normal file
@@ -0,0 +1,108 @@
|
||||
"""Methods for scraping the debian website for specific file URLs."""
|
||||
|
||||
from re import compile
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
|
||||
def get_debian_preseed_file_urls():
|
||||
"""Returns a dict containing the URLs for the debian example preseed files.
|
||||
|
||||
The dict has the following structure:
|
||||
{
|
||||
"basic": {
|
||||
"url": "https://...",
|
||||
"name": "...",
|
||||
},
|
||||
"full": {
|
||||
"url": "https://...",
|
||||
"name": "...",
|
||||
},
|
||||
}
|
||||
where "basic" points to the basic preseed file and its filename, and "full"
|
||||
points to the full preseed file and its filename.
|
||||
"""
|
||||
|
||||
preseed_file_urls = {
|
||||
"basic": {
|
||||
"url": "https://www.debian.org/releases/stable/example-preseed.txt",
|
||||
"name": "example-preseed.txt",
|
||||
},
|
||||
"full": {
|
||||
"url": "https://preseed.debian.net/debian-preseed/bullseye/amd64-main-full.txt",
|
||||
"name": "amd64-main-full.txt",
|
||||
},
|
||||
}
|
||||
|
||||
return preseed_file_urls
|
||||
|
||||
|
||||
def get_debian_iso_urls():
|
||||
"""Retrieves a dict containing the URLs for a debian installation image.
|
||||
|
||||
The dict has the following structure:
|
||||
{
|
||||
"image_file": {
|
||||
"url": "https://...",
|
||||
"name": "debian-xx.x.x-amd64-netinst.iso",
|
||||
},
|
||||
"hash_file": {
|
||||
"url": "https://...",
|
||||
"name": "SHA512SUMS",
|
||||
},
|
||||
"signature_file": {
|
||||
"url": "https://...",
|
||||
"name": "SHA512SUMS.sign",
|
||||
},
|
||||
}
|
||||
where "image_file" is points to the latest debian stable x86-64bit
|
||||
net-installation ISO image, "hash_file" points to a SHA512SUMS file
|
||||
containing the SHA512 checksum for the ISO file, and "signature_file"
|
||||
points to a file containing a PGP signature for verification of the
|
||||
SHA512SUMS file.
|
||||
Each top-level dict entry contains a "name" key representing a file name,
|
||||
and a "url" key specifying a URL to that file.
|
||||
|
||||
The function scrapes the official debian.org website to retrieve the URLs.
|
||||
"""
|
||||
|
||||
# request the debian releases page
|
||||
releases_url = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/"
|
||||
releases_page = requests.get(releases_url)
|
||||
if not releases_page.status_code == 200:
|
||||
raise RuntimeError("Unexpected status code during request.")
|
||||
|
||||
hash_file_name = "SHA512SUMS"
|
||||
hash_file_url = releases_url + hash_file_name
|
||||
signature_file_name = "SHA512SUMS.sign"
|
||||
signature_file_url = releases_url + signature_file_name
|
||||
|
||||
# find the exact URL to the latest stable x64 netinst ISO file
|
||||
soup = BeautifulSoup(releases_page.content, "html.parser")
|
||||
image_file_links = soup.find_all(
|
||||
name="a",
|
||||
string=compile(r"debian-[0-9.]*-amd64-netinst.iso")
|
||||
)
|
||||
if len(image_file_links) != 1:
|
||||
raise RuntimeError(
|
||||
"Failed to find an exact match while looking for "
|
||||
"a link to the latest debian image file."
|
||||
)
|
||||
image_file_name = image_file_links[0]['href']
|
||||
image_file_url = releases_url + image_file_name
|
||||
|
||||
return {
|
||||
"image_file": {
|
||||
"url": image_file_url,
|
||||
"name": image_file_name,
|
||||
},
|
||||
"hash_file": {
|
||||
"url": hash_file_url,
|
||||
"name": hash_file_name,
|
||||
},
|
||||
"signature_file": {
|
||||
"url": signature_file_url,
|
||||
"name": signature_file_name,
|
||||
},
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
beautifulsoup4==4.11.1
|
||||
certifi==2021.10.8
|
||||
charset-normalizer==2.0.12
|
||||
colorama==0.4.4
|
||||
bs4==0.0.1
|
||||
certifi==2022.6.15
|
||||
charset-normalizer==2.1.0
|
||||
colorama==0.4.5
|
||||
idna==3.3
|
||||
python-gnupg==0.4.8
|
||||
requests==2.27.1
|
||||
requests==2.28.1
|
||||
soupsieve==2.3.2.post1
|
||||
tqdm==4.64.0
|
||||
urllib3==1.26.9
|
||||
urllib3==1.26.11
|
||||
|
||||
162
udib.py
Executable file
162
udib.py
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Main entry point for the interactive udib CLI tool."""
|
||||
|
||||
from pathlib import Path
|
||||
from sys import exit
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
from cli.clibella import Printer
|
||||
from cli.parser import get_argument_parser
|
||||
from core.utils import download_and_verify_debian_iso
|
||||
from iso.injection import inject_preseed_file_into_iso
|
||||
from net.download import download_file
|
||||
from net.scrape import get_debian_preseed_file_urls, get_debian_iso_urls
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
# create a CLI printer
|
||||
p = Printer()
|
||||
|
||||
# create an argument parser and read arguments
|
||||
parser = get_argument_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# parse and verify output file if sepcified
|
||||
if args.path_to_output_file:
|
||||
path_to_output_file = Path(args.path_to_output_file)
|
||||
if "~" in str(path_to_output_file):
|
||||
path_to_output_file = path_to_output_file.expanduser()
|
||||
path_to_output_file = path_to_output_file.resolve()
|
||||
|
||||
if path_to_output_file.exists():
|
||||
p.error(f"Output file already exists: '{path_to_output_file}'.")
|
||||
exit(1)
|
||||
else:
|
||||
path_to_output_file = None
|
||||
|
||||
# parse and verify output dir if sepcified
|
||||
if args.path_to_output_dir:
|
||||
path_to_output_dir = Path(args.path_to_output_dir)
|
||||
if "~" in str(path_to_output_dir):
|
||||
path_to_output_dir = path_to_output_dir.expanduser()
|
||||
path_to_output_dir = path_to_output_dir.resolve()
|
||||
|
||||
if not path_to_output_dir.is_dir():
|
||||
p.error(f"No such directory: '{path_to_output_dir}'.")
|
||||
exit(1)
|
||||
else:
|
||||
path_to_output_dir = None
|
||||
|
||||
if args.subparser_name == "get":
|
||||
if args.WHAT == "preseed-file-basic":
|
||||
# download the basic example preseedfile
|
||||
|
||||
if not path_to_output_file:
|
||||
output_file_name = get_debian_preseed_file_urls()["basic"]["name"]
|
||||
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
|
||||
|
||||
p.info("Retrieving basic preseed example file...")
|
||||
download_file(
|
||||
path_to_output_file,
|
||||
get_debian_preseed_file_urls()["basic"]["url"],
|
||||
show_progress=False,
|
||||
printer=p,
|
||||
)
|
||||
p.success(
|
||||
f"Basic preseed example file was saved to '{path_to_output_file}'."
|
||||
)
|
||||
exit(0)
|
||||
|
||||
elif args.WHAT == "preseed-file-full":
|
||||
# download the full example preseedfile
|
||||
|
||||
if not path_to_output_file:
|
||||
output_file_name = get_debian_preseed_file_urls()["full"]["name"]
|
||||
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
|
||||
|
||||
p.info("Retrieving full preseed example file...")
|
||||
download_file(
|
||||
path_to_output_file,
|
||||
get_debian_preseed_file_urls()["full"]["url"],
|
||||
show_progress=False,
|
||||
printer=p,
|
||||
)
|
||||
p.success(
|
||||
f"Full preseed example file was saved to '{path_to_output_file}'."
|
||||
)
|
||||
exit(0)
|
||||
|
||||
elif args.WHAT == "iso":
|
||||
# download and verify installation image
|
||||
p.info("Downloading latest Debian stable x86-64 netinst ISO...")
|
||||
|
||||
if not path_to_output_file:
|
||||
output_file_name = get_debian_iso_urls()["image_file"]["name"]
|
||||
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
|
||||
|
||||
download_and_verify_debian_iso(path_to_output_file, printer=p)
|
||||
p.success(f"Debian ISO saved to '{path_to_output_file}'.")
|
||||
exit(0)
|
||||
|
||||
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
|
||||
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 image file path if set by user or download fresh iso if unset
|
||||
temp_iso_dir = None
|
||||
if args.path_to_image_file:
|
||||
path_to_image_file = Path(args.path_to_image_file)
|
||||
if "~" in str(path_to_image_file):
|
||||
path_to_image_file = Path(path_to_image_file).expanduser()
|
||||
path_to_image_file = Path(path_to_image_file).resolve()
|
||||
if not path_to_image_file.is_file():
|
||||
p.error(f"No such file: '{path_to_image_file}'.")
|
||||
exit(1)
|
||||
else:
|
||||
# download a Debian ISO to a temporary directory
|
||||
p.info("Downloading the latest Debian x86-64 netinst image...")
|
||||
temp_iso_dir = TemporaryDirectory()
|
||||
path_to_iso_dir = Path(temp_iso_dir.name)
|
||||
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(
|
||||
path_to_output_file,
|
||||
path_to_image_file,
|
||||
path_to_preseed_file,
|
||||
printer=p,
|
||||
)
|
||||
|
||||
# clear out temporary directory if one was created earlier
|
||||
if temp_iso_dir:
|
||||
temp_iso_dir.cleanup()
|
||||
|
||||
exit(0)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
@@ -1,130 +0,0 @@
|
||||
"""Library for verification of GPG signatures."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import gnupg
|
||||
|
||||
import userinput
|
||||
import clibella
|
||||
|
||||
|
||||
p = clibella.Printer()
|
||||
|
||||
|
||||
def gpg_signature_is_valid(
|
||||
path_to_signature_file,
|
||||
path_to_data_file,
|
||||
fallback_keyserver_name
|
||||
):
|
||||
"""Validates a PGP signature for a data file.
|
||||
|
||||
The detached signature must be provided as plaintext (UTF8) in the
|
||||
specified signature file.
|
||||
If the discovered signing key is unknown to gpg on this system for the
|
||||
invoking user, and if 'fallback_keyserver_name' is not None, an attempt is
|
||||
made to import the key from the specified keyserver using its ID after
|
||||
prompting the user for permission to import the key.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_to_signature_file : str or pathlike object
|
||||
Path to a detached PGP signature stored in a standalone file.
|
||||
Example value: "/path/to/SHA256SUMS.sig"
|
||||
path_to_data_file : str or pathlike object
|
||||
Path to a signed data file, for which the signature is to be verified.
|
||||
Example value: "/path/to/SHA256SUMS"
|
||||
fallback_keyserver_name : str
|
||||
FQDN of a keyserver from which to import unknown public keys.
|
||||
Example value: "keyring.debian.org"
|
||||
|
||||
Returns
|
||||
-------
|
||||
True : bool
|
||||
If the signature is valid.
|
||||
False : bool
|
||||
If the signature is invalid or could not be validated.
|
||||
"""
|
||||
|
||||
path_to_signature_file = Path(path_to_signature_file)
|
||||
path_to_data_file = Path(path_to_data_file)
|
||||
|
||||
gpg = gnupg.GPG()
|
||||
gpg.encoding = "utf-8"
|
||||
|
||||
p.info("Validating signature...")
|
||||
with open(path_to_signature_file, "rb") as signature_file:
|
||||
verification = gpg.verify_file(
|
||||
signature_file,
|
||||
path_to_data_file,
|
||||
close_file=False
|
||||
)
|
||||
|
||||
# check if a key and fingerprint were found in the signature file
|
||||
if verification.key_id is None or verification.fingerprint is None:
|
||||
raise ValueError(
|
||||
f"Not a valid PGP signature file: '{path_to_signature_file}'."
|
||||
)
|
||||
else:
|
||||
p.info(f"Signature mentions a key with ID "
|
||||
f"{verification.key_id} and fingerprint "
|
||||
f"{verification.fingerprint}."
|
||||
)
|
||||
|
||||
if verification.valid:
|
||||
p.ok(f"GPG signature is valid with trustlevel "
|
||||
f"'{verification.trust_level}'."
|
||||
)
|
||||
return True
|
||||
|
||||
# this case commonly occurrs when the GPG key has not been imported
|
||||
if verification.status == "no public key":
|
||||
p.warning("Could not find the public GPG key locally!")
|
||||
|
||||
# prompt user until answer is unambiguous
|
||||
key_will_be_imported = None
|
||||
while key_will_be_imported is None:
|
||||
key_will_be_imported = userinput.prompt_yes_or_no(
|
||||
f"[PROMPT] Do you want to import the GPG key from "
|
||||
f"'{fallback_keyserver_name}'?"
|
||||
)
|
||||
|
||||
if key_will_be_imported is None:
|
||||
p.error("Unrecognized input. Please try again.")
|
||||
|
||||
if not key_will_be_imported:
|
||||
p.warning("Aborting without importing key.")
|
||||
return False
|
||||
|
||||
# import missing key
|
||||
p.info("Importing key...")
|
||||
import_result = gpg.recv_keys(
|
||||
fallback_keyserver_name, verification.key_id
|
||||
)
|
||||
|
||||
if import_result.count < 1:
|
||||
p.error("Failed to import key.")
|
||||
return False
|
||||
|
||||
# display some of gpg's output
|
||||
gpg_output = import_result.stderr.split('\n')
|
||||
for line in gpg_output:
|
||||
if line.startswith("gpg: "):
|
||||
p.info(f"{line}")
|
||||
|
||||
# validate signature again
|
||||
p.info("Validating signature...")
|
||||
with open(path_to_signature_file, "rb") as signature_file:
|
||||
verification = gpg.verify_file(
|
||||
signature_file,
|
||||
path_to_data_file,
|
||||
close_file=False
|
||||
)
|
||||
|
||||
if verification.valid:
|
||||
p.ok(f"GPG signature is valid with trustlevel "
|
||||
f"'{verification.trust_level}'."
|
||||
)
|
||||
return True
|
||||
else:
|
||||
p.error("GPG signature is not valid!!!")
|
||||
return False
|
||||
333
udib/udib.py
333
udib/udib.py
@@ -1,333 +0,0 @@
|
||||
#!/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",
|
||||
"gpg",
|
||||
"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 capture ISO filesystem name
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
help="Path to which the resulting file is written.",
|
||||
type=str,
|
||||
dest="path_to_output_file",
|
||||
action="store")
|
||||
|
||||
subparsers = parser.add_subparsers(required=True)
|
||||
|
||||
parser_get = subparsers.add_parser("get")
|
||||
parser_get.add_argument(
|
||||
"WHAT",
|
||||
type=str,
|
||||
action="store",
|
||||
help="What to retrieve (the latest installation image or the latest " \
|
||||
"preseed file).",
|
||||
choices=["image", "preseed-file"])
|
||||
|
||||
parser_insert = subparsers.add_parser("insert")
|
||||
parser_insert.add_argument(
|
||||
"FILES",
|
||||
nargs="+",
|
||||
type=str,
|
||||
help="A list of files to insert into the root of an installation " \
|
||||
"image.",
|
||||
action="store")
|
||||
parser_insert.add_argument(
|
||||
"-i",
|
||||
"--existing-image",
|
||||
help="Insert into an existing debian image file, instead of " \
|
||||
"downloading 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"
|
||||
|
||||
# FIXME adjust to new parser namespace
|
||||
|
||||
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()
|
||||
@@ -1,50 +0,0 @@
|
||||
"""Library for consistent interactive user input capturing."""
|
||||
|
||||
import re
|
||||
|
||||
import clibella
|
||||
|
||||
|
||||
p = clibella.Printer()
|
||||
|
||||
|
||||
def prompt_yes_or_no(question, ask_until_valid=False):
|
||||
"""Prompts the user for an answer to the specified yes/no question.
|
||||
|
||||
If 'ask_until_valid' is True, keeps repeating the prompt until the user
|
||||
provides recognizable input to answer the question.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
question : str
|
||||
The question to ask the user while prompting. The string is suffixed
|
||||
with " (Yes/No) ".
|
||||
ask_until_valid : bool
|
||||
If this is set to True and the user provides an ambiguous answer, keeps
|
||||
prompting indefinitely until an unambiguous answer is read.
|
||||
|
||||
Returns
|
||||
-------
|
||||
True : bool
|
||||
If the user has answered 'yes'.
|
||||
False : bool
|
||||
If the user has answered 'no'.
|
||||
None : None
|
||||
If 'ask_until_valid' is False and the user has provided an ambiguous
|
||||
response to the prompt.
|
||||
"""
|
||||
|
||||
user_input_is_valid = False
|
||||
|
||||
regex_yes = re.compile(r"^(y)$|^(Y)$|^(YES)$|^(Yes)$|^(yes)$")
|
||||
regex_no = re.compile(r"^(n)$|^(N)$|^(NO)$|^(No)$|^(no)$")
|
||||
|
||||
while(not user_input_is_valid):
|
||||
user_input = p.input(f"{question} (Yes/No): ")
|
||||
|
||||
if (regex_yes.match(user_input)):
|
||||
return True
|
||||
elif (regex_no.match(user_input)):
|
||||
return False
|
||||
elif (not ask_until_valid):
|
||||
return None
|
||||
@@ -1,197 +0,0 @@
|
||||
"""Library for downloading files from the web.
|
||||
|
||||
Specifically, debian installation images and any accompanying files
|
||||
required for verification of authenticity and integrity of the
|
||||
obtained images.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
from bs4 import BeautifulSoup
|
||||
from tqdm import tqdm
|
||||
|
||||
import clibella
|
||||
import gpgverify
|
||||
|
||||
|
||||
p = clibella.Printer()
|
||||
|
||||
|
||||
def download_file(path_to_output_file, url_to_file, show_progress=False):
|
||||
"""Downloads the file at the input URL to the specified path.
|
||||
|
||||
The file is downloaded via HTTP/HTTPS and saved to the specified path.
|
||||
Optionally, displays a nice status bar.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_to_output_file : str or pathlike object
|
||||
Path to a file as which the downloaded file is saved.
|
||||
url_to_file : str
|
||||
URL to the file to be downloaded.
|
||||
show_progress : bool
|
||||
When True, a progress bar is displayed on StdOut indicating the
|
||||
progress of the download.
|
||||
"""
|
||||
|
||||
path_to_output_file = Path(path_to_output_file).resolve()
|
||||
if not path_to_output_file.parent.is_dir():
|
||||
raise FileNotFoundError(
|
||||
f"No such directory: '{path_to_output_file.parent}'."
|
||||
)
|
||||
if path_to_output_file.exists():
|
||||
raise FileExistsError(
|
||||
f"File already exists: '{path_to_output_file}'"
|
||||
)
|
||||
|
||||
output_file_name = path_to_output_file.name
|
||||
with open(path_to_output_file, "wb") as output_file:
|
||||
p.info(f"Downloading '{output_file_name}'...")
|
||||
file_response = requests.get(url_to_file, stream=True)
|
||||
total_length = file_response.headers.get('content-length')
|
||||
|
||||
if total_length is None: # no content length header
|
||||
output_file.write(file_response.content)
|
||||
else:
|
||||
if (show_progress):
|
||||
total_length = int(total_length)
|
||||
progress_bar = tqdm(
|
||||
total=total_length,
|
||||
unit="B",
|
||||
unit_scale=True,
|
||||
unit_divisor=1024
|
||||
)
|
||||
|
||||
for data in file_response.iter_content(chunk_size=4096):
|
||||
output_file.write(data)
|
||||
if (show_progress):
|
||||
progress_bar.update(len(data))
|
||||
|
||||
if (show_progress):
|
||||
progress_bar.close()
|
||||
|
||||
p.ok(f"Received '{output_file_name}'.")
|
||||
|
||||
|
||||
def debian_obtain_image(path_to_output_dir):
|
||||
"""Downloads the latest debian installation image and its hashes.
|
||||
|
||||
The image file, the SHA512SUMS file it is listed in, as well as the GPG
|
||||
signature for the SHA512SUMS file are downloaded from the debian.org HTTPS
|
||||
mirrors and written into the specified output directory.
|
||||
The obtained image is for the FOSS-only, stable x64 build.
|
||||
|
||||
Once the image file is downloaded, the GPG signature of the hash is
|
||||
validated. Then, the hash of the image file is verified. If either check
|
||||
fails, an exception is raised.
|
||||
|
||||
If the verification suceeds, the hash file and GPG signature file are
|
||||
removed again.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
path_to_output_dir : str or pathlike object
|
||||
Path to the directory to which all downloaded files will be saved.
|
||||
|
||||
Returns
|
||||
-------
|
||||
path_to_image_file : str
|
||||
Full path to the obtained and validated image file.
|
||||
"""
|
||||
|
||||
# FIXME download files to a temporary directory
|
||||
p.info("Obtaining and verifying the latest Debian stable image...")
|
||||
|
||||
path_to_output_dir = Path(path_to_output_dir)
|
||||
|
||||
if not path_to_output_dir.is_dir():
|
||||
raise ValueError(f"No such directory: '{path_to_output_dir}'")
|
||||
|
||||
releases_url = "https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/"
|
||||
releases_page = requests.get(releases_url)
|
||||
soup = BeautifulSoup(releases_page.content, "html.parser")
|
||||
|
||||
hash_file_name = "SHA512SUMS"
|
||||
hash_file_url = releases_url + hash_file_name
|
||||
signature_file_name = "SHA512SUMS.sign"
|
||||
signature_file_url = releases_url + signature_file_name
|
||||
|
||||
# find the URL to the latest stable x64 image
|
||||
image_file_links = soup.find_all(
|
||||
name="a",
|
||||
string=re.compile(r"debian-[0-9.]*-amd64-netinst.iso"))
|
||||
if len(image_file_links) != 1:
|
||||
raise RuntimeError("Failed to find an exact match while looking for "
|
||||
"a link to the latest debian image file."
|
||||
)
|
||||
image_file_name = image_file_links[0]['href']
|
||||
image_file_url = releases_url + image_file_name
|
||||
|
||||
# download the SHA512SUMS file
|
||||
download_file(path_to_output_dir/hash_file_name, hash_file_url)
|
||||
# download the GPG signature file
|
||||
download_file(path_to_output_dir/signature_file_name, signature_file_url)
|
||||
|
||||
# verify GPG signature of hash file
|
||||
if not gpgverify.gpg_signature_is_valid(
|
||||
path_to_output_dir/signature_file_name,
|
||||
path_to_output_dir/hash_file_name,
|
||||
"keyring.debian.org"
|
||||
):
|
||||
raise RuntimeError("GPG signature verification failed!")
|
||||
|
||||
# download the image file
|
||||
download_file(path_to_output_dir/image_file_name, image_file_url, True)
|
||||
|
||||
# remove unwanted lines from hash file
|
||||
hash_file = open(path_to_output_dir/hash_file_name, "r")
|
||||
hash_file_lines = hash_file.readlines()
|
||||
hash_file_lines_to_keep = []
|
||||
for line in hash_file_lines:
|
||||
if image_file_name in line:
|
||||
hash_file_lines_to_keep.append(line)
|
||||
hash_file.close()
|
||||
if len(hash_file_lines_to_keep) != 1:
|
||||
raise RuntimeError("Unexpected error while truncating hash file.")
|
||||
os.remove(path_to_output_dir/hash_file_name)
|
||||
with open(path_to_output_dir/hash_file_name, "w") as hash_file:
|
||||
hash_file.writelines(hash_file_lines_to_keep)
|
||||
|
||||
# validate SHA512 checksum
|
||||
p.info("Validating file integrity...")
|
||||
hash_check_result = subprocess.run(
|
||||
["sha512sum", "--check", path_to_output_dir/hash_file_name],
|
||||
capture_output=True,
|
||||
cwd=path_to_output_dir
|
||||
)
|
||||
|
||||
stdout_lines = hash_check_result.stdout.decode("utf-8").split('\n')
|
||||
stderr_lines = hash_check_result.stderr.decode("utf-8").split('\n')
|
||||
|
||||
if len(stdout_lines) > 0:
|
||||
for line in stdout_lines:
|
||||
if len(line) > 0:
|
||||
p.info(f"{line}")
|
||||
|
||||
if hash_check_result.returncode != 0:
|
||||
if len(stderr_lines) > 0:
|
||||
for line in stderr_lines:
|
||||
if len(line) > 0:
|
||||
p.error(f"{line}")
|
||||
raise RuntimeError("SHA512 validation failed.")
|
||||
|
||||
p.ok("File integrity checks passed.")
|
||||
|
||||
# clean up obsolete files
|
||||
p.info("Cleaning up files...")
|
||||
os.remove(path_to_output_dir/hash_file_name)
|
||||
os.remove(path_to_output_dir/signature_file_name)
|
||||
|
||||
p.success("Debian image obtained.")
|
||||
|
||||
return path_to_output_dir/image_file_name
|
||||
Reference in New Issue
Block a user