├── .github ├── dependabot.yml └── workflows │ ├── directory-bootstrap-alpine.yml │ ├── directory-bootstrap-arch.yml │ ├── directory-bootstrap-gentoo.yml │ ├── directory-bootstrap-void.yml │ ├── image-bootstrap-arch.yml │ ├── image-bootstrap-debian.yml │ ├── image-bootstrap-gentoo.yml │ ├── image-bootstrap-ubuntu.yml │ └── run-test-suite.yml ├── .gitignore ├── README.md ├── debian ├── .gitignore ├── changelog ├── compat ├── control ├── copyright ├── rules └── source │ └── format ├── directory_bootstrap ├── .gitignore ├── MANIFEST.in ├── __init__.py ├── __main__.py ├── distros │ ├── __init__.py │ ├── alpine.py │ ├── arch.py │ ├── base.py │ ├── gentoo.py │ └── void.py ├── resources │ ├── __init__.py │ ├── alpine │ │ ├── __init__.py │ │ └── ncopa.asc │ └── gentoo │ │ ├── 13EBBDBEDE7A12775DFDB1BABB572E0E2D182910.asc │ │ ├── 18F703D702B1B9591373148C55D3238EC050396E.asc │ │ ├── 2C13823B8237310FA213034930D132FF0FF50EEB.asc │ │ ├── ABD00913019D6354BA1D9A132839FE0D796198B1.asc │ │ ├── D99EAC7379A850BCE47DA5F29E6438C817072058.asc │ │ ├── DCD05B71EAB94199527F44ACDB6B8C1F96D8BF6D.asc │ │ ├── EF9538C9E8E64311A52CDEDFA13D0EF1914E7A72.asc │ │ └── __init__.py ├── setup-pypi-readme.rst ├── setup.py ├── shared │ ├── __init__.py │ ├── byte_size.py │ ├── commands.py │ ├── executor.py │ ├── loaders │ │ ├── __init__.py │ │ ├── _argparse.py │ │ ├── _bs4.py │ │ ├── _colorama.py │ │ └── _requests.py │ ├── messenger.py │ ├── metadata.py │ ├── mount.py │ ├── namespace.py │ ├── output_control.py │ ├── resolv_conf.py │ └── test │ │ ├── __init__.py │ │ ├── test_byte_size.py │ │ └── test_path_extension.py └── tools │ ├── __init__.py │ ├── stage3_latest_parser.py │ └── test │ ├── __init__.py │ └── test_stage3_latest_parser.py ├── image_bootstrap ├── __init__.py ├── __main__.py ├── boot_loaders │ ├── __init__.py │ └── grub2.py ├── distros │ ├── __init__.py │ ├── arch.py │ ├── base.py │ ├── debian.py │ ├── debian_based.py │ ├── gentoo.py │ └── ubuntu.py ├── engine.py ├── loaders │ ├── __init__.py │ └── _yaml.py ├── mount.py ├── test │ ├── __init__.py │ └── test_mount.py └── types │ ├── __init__.py │ ├── disk_id.py │ ├── machine_id.py │ └── uuid.py ├── requirements.txt ├── scripts └── debug.sh ├── setup.py └── test.sh /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | - package-ecosystem: "github-actions" 5 | commit-message: 6 | include: "scope" 7 | prefix: "Actions" 8 | directory: "/" 9 | labels: 10 | - "enhancement" 11 | schedule: 12 | interval: "weekly" 13 | 14 | - package-ecosystem: "pip" 15 | commit-message: 16 | include: "scope" 17 | prefix: "requirements" 18 | directory: "/" 19 | labels: 20 | - "dependencies" 21 | - "enhancement" 22 | schedule: 23 | interval: "daily" 24 | -------------------------------------------------------------------------------- /.github/workflows/directory-bootstrap-alpine.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Alpine chroots 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Alpine chroots 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout Git repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Cache pip 18 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | 30 | - name: Install 31 | run: |- 32 | sudo pip3 install \ 33 | --disable-pip-version-check \ 34 | . 35 | 36 | - name: Smoke test creation of Alpine chroots 37 | run: |- 38 | cd /tmp # to not be in Git clone folder 39 | 40 | directory-bootstrap --help ; echo 41 | directory-bootstrap alpine --help ; echo 42 | 43 | sudo PYTHONUNBUFFERED=1 directory-bootstrap --verbose --debug alpine /tmp/alpine_chroot/ 44 | 45 | - name: Create .tar archive 46 | run: |- 47 | set -eux 48 | git fetch --force --tags --unshallow origin # for "git describe" 49 | chroot_base_name="alpine-chroot-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).tar.xz" 50 | sudo chmod a+xr /tmp/alpine_chroot/ # for "cd" 51 | ( cd /tmp/alpine_chroot/ && sudo tar c . ) | xz -T "$(nproc)" > "${chroot_base_name}" 52 | ls -lh "${chroot_base_name}" 53 | 54 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 55 | with: 56 | name: alpine-chroot-qcow2 57 | path: '*.tar.xz' 58 | if-no-files-found: error 59 | -------------------------------------------------------------------------------- /.github/workflows/directory-bootstrap-arch.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Arch chroots 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Arch chroots 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout Git repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Cache pip 18 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | 30 | - name: Install 31 | run: |- 32 | sudo pip3 install \ 33 | --disable-pip-version-check \ 34 | . 35 | 36 | - name: Smoke test creation of Arch chroots 37 | run: |- 38 | cd /tmp # to not be in Git clone folder 39 | 40 | directory-bootstrap --help ; echo 41 | directory-bootstrap arch --help ; echo 42 | 43 | sudo PYTHONUNBUFFERED=1 directory-bootstrap --verbose --debug arch /tmp/arch_chroot/ 44 | 45 | - name: Create .tar archive 46 | run: |- 47 | set -eux 48 | git fetch --force --tags --unshallow origin # for "git describe" 49 | chroot_base_name="arch-chroot-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).tar.xz" 50 | sudo chmod a+xr /tmp/arch_chroot/ # for "cd" 51 | ( cd /tmp/arch_chroot/ && sudo tar c . ) | xz -T "$(nproc)" > "${chroot_base_name}" 52 | ls -lh "${chroot_base_name}" 53 | 54 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 55 | with: 56 | name: arch-chroot-qcow2 57 | path: '*.tar.xz' 58 | if-no-files-found: error 59 | -------------------------------------------------------------------------------- /.github/workflows/directory-bootstrap-gentoo.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Gentoo chroots 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Gentoo chroots 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout Git repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Cache pip 18 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | 30 | - name: Install 31 | run: |- 32 | sudo pip3 install \ 33 | --disable-pip-version-check \ 34 | . 35 | 36 | - name: Smoke test creation of Gentoo chroots 37 | run: |- 38 | cd /tmp # to not be in Git clone folder 39 | 40 | directory-bootstrap --help ; echo 41 | directory-bootstrap gentoo --help ; echo 42 | 43 | sudo PYTHONUNBUFFERED=1 directory-bootstrap --verbose --debug gentoo /tmp/gentoo_chroot/ 44 | 45 | - name: Create .tar archive 46 | run: |- 47 | set -eux 48 | git fetch --force --tags --unshallow origin # for "git describe" 49 | chroot_base_name="gentoo-chroot-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).tar.xz" 50 | sudo chmod a+xr /tmp/gentoo_chroot/ # for "cd" 51 | ( cd /tmp/gentoo_chroot/ && sudo tar c . ) | xz -T "$(nproc)" > "${chroot_base_name}" 52 | ls -lh "${chroot_base_name}" 53 | 54 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 55 | with: 56 | name: gentoo-chroot-qcow2 57 | path: '*.tar.xz' 58 | if-no-files-found: error 59 | -------------------------------------------------------------------------------- /.github/workflows/directory-bootstrap-void.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Void chroots 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Void chroots 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout Git repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Cache pip 18 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | 30 | - name: Install 31 | run: |- 32 | sudo pip3 install \ 33 | --disable-pip-version-check \ 34 | . 35 | 36 | - name: Smoke test creation of Void chroots 37 | run: |- 38 | cd /tmp # to not be in Git clone folder 39 | 40 | directory-bootstrap --help ; echo 41 | directory-bootstrap void --help ; echo 42 | 43 | sudo PYTHONUNBUFFERED=1 directory-bootstrap --verbose --debug void /tmp/void_chroot/ 44 | 45 | - name: Create .tar archive 46 | run: |- 47 | set -eux 48 | git fetch --force --tags --unshallow origin # for "git describe" 49 | chroot_base_name="void-chroot-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).tar.xz" 50 | sudo chmod a+xr /tmp/void_chroot/ # for "cd" 51 | ( cd /tmp/void_chroot/ && sudo tar c . ) | xz -T "$(nproc)" > "${chroot_base_name}" 52 | ls -lh "${chroot_base_name}" 53 | 54 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 55 | with: 56 | name: void-chroot-qcow2 57 | path: '*.tar.xz' 58 | if-no-files-found: error 59 | -------------------------------------------------------------------------------- /.github/workflows/image-bootstrap-arch.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Arch OpenStack images 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Arch OpenStack images 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout Git repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Cache pip 18 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | 30 | - name: Install 31 | run: |- 32 | sudo pip3 install \ 33 | --disable-pip-version-check \ 34 | . 35 | 36 | - name: Install runtime dependencies 37 | run: |- 38 | sudo apt-get update 39 | sudo apt-get install --no-install-recommends --yes \ 40 | kpartx \ 41 | qemu-utils 42 | 43 | - name: Smoke test creation of Arch OpenStack images 44 | run: |- 45 | cd /tmp # to not be in Git clone folder 46 | 47 | image-bootstrap --help ; echo 48 | image-bootstrap arch --help ; echo 49 | 50 | truncate --size 3g /tmp/disk 51 | LOOP_DEV="$(sudo losetup --show --find -f /tmp/disk | tee /dev/stderr)" 52 | echo "LOOP_DEV=${LOOP_DEV}" >> "${GITHUB_ENV}" 53 | 54 | sudo PYTHONUNBUFFERED=1 image-bootstrap --verbose --debug --openstack arch ${LOOP_DEV} 55 | 56 | - name: Create .qcow2 image from loop device 57 | run: |- 58 | set -eux 59 | git fetch --force --tags --unshallow origin # for "git describe" 60 | img_base_name="arch-openstack-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).qcow2" 61 | sudo qemu-img convert -f raw -O qcow2 "${LOOP_DEV}" "${img_base_name}" 62 | ls -lh "${img_base_name}" 63 | 64 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 65 | with: 66 | name: arch-openstack-qcow2 67 | path: '*.qcow2' 68 | if-no-files-found: error 69 | -------------------------------------------------------------------------------- /.github/workflows/image-bootstrap-debian.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Debian OpenStack images 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Debian OpenStack images 12 | strategy: 13 | matrix: 14 | debian_release: 15 | # https://www.debian.org/releases/ 16 | - bullseye 17 | - bookworm 18 | runs-on: ubuntu-22.04 19 | steps: 20 | - name: Checkout Git repository 21 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 22 | 23 | - name: Cache pip 24 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 25 | with: 26 | path: ~/.cache/pip 27 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 28 | restore-keys: | 29 | ${{ runner.os }}-pip- 30 | 31 | - name: Set up Python 3.13 32 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 33 | with: 34 | python-version: 3.13 35 | 36 | - name: Install 37 | run: |- 38 | sudo pip3 install \ 39 | --disable-pip-version-check \ 40 | . 41 | 42 | - name: Install runtime dependencies 43 | run: |- 44 | sudo apt-get update 45 | sudo apt-get install --no-install-recommends --yes \ 46 | debian-archive-keyring \ 47 | debootstrap \ 48 | kpartx \ 49 | qemu-utils 50 | 51 | - name: Smoke test creation of Debian OpenStack images 52 | run: |- 53 | cd /tmp # to not be in Git clone folder 54 | 55 | image-bootstrap --help ; echo 56 | image-bootstrap debian --help ; echo 57 | 58 | truncate --size 2g /tmp/disk 59 | LOOP_DEV="$(sudo losetup --show --find -f /tmp/disk | tee /dev/stderr)" 60 | echo "LOOP_DEV=${LOOP_DEV}" >> "${GITHUB_ENV}" 61 | 62 | sudo PYTHONUNBUFFERED=1 image-bootstrap --verbose --debug --openstack debian --release ${{ matrix.debian_release }} ${LOOP_DEV} 63 | 64 | - name: Create .qcow2 image from loop device 65 | run: |- 66 | set -eux 67 | git fetch --force --tags --unshallow origin # for "git describe" 68 | img_base_name="debian-openstack-${{ matrix.debian_release }}-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).qcow2" 69 | sudo qemu-img convert -f raw -O qcow2 "${LOOP_DEV}" "${img_base_name}" 70 | ls -lh "${img_base_name}" 71 | 72 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 73 | with: 74 | name: debian-${{ matrix.debian_release }}-openstack-qcow2 75 | path: '*.qcow2' 76 | if-no-files-found: error 77 | -------------------------------------------------------------------------------- /.github/workflows/image-bootstrap-gentoo.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Gentoo OpenStack images 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Gentoo OpenStack images 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout Git repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Cache pip 18 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | 30 | - name: Install 31 | run: |- 32 | sudo pip3 install \ 33 | --disable-pip-version-check \ 34 | . 35 | 36 | - name: Install runtime dependencies 37 | run: |- 38 | sudo apt-get update 39 | sudo apt-get install --no-install-recommends --yes \ 40 | kpartx \ 41 | qemu-utils 42 | 43 | - name: Smoke test creation of Gentoo OpenStack images 44 | run: |- 45 | cd /tmp # to not be in Git clone folder 46 | 47 | image-bootstrap --help ; echo 48 | image-bootstrap gentoo --help ; echo 49 | 50 | free -g 51 | df -H 52 | 53 | truncate --size 7g /tmp/disk 54 | LOOP_DEV="$(sudo losetup --show --find -f /tmp/disk | tee /dev/stderr)" 55 | echo "LOOP_DEV=${LOOP_DEV}" >> "${GITHUB_ENV}" 56 | 57 | sudo PYTHONUNBUFFERED=1 image-bootstrap --verbose --debug --openstack gentoo ${LOOP_DEV} 58 | 59 | - name: Create .qcow2 image from loop device 60 | run: |- 61 | set -eux 62 | git fetch --force --tags --unshallow origin # for "git describe" 63 | img_base_name="gentoo-openstack-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).qcow2" 64 | sudo qemu-img convert -f raw -O qcow2 "${LOOP_DEV}" "${img_base_name}" 65 | ls -lh "${img_base_name}" 66 | 67 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 68 | with: 69 | name: gentoo-openstack-qcow2 70 | path: '*.qcow2' 71 | if-no-files-found: error 72 | -------------------------------------------------------------------------------- /.github/workflows/image-bootstrap-ubuntu.yml: -------------------------------------------------------------------------------- 1 | name: Smoke test creation of Ubuntu OpenStack images 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | 9 | jobs: 10 | install_and_run: 11 | name: Smoke test creation of Ubuntu OpenStack images 12 | runs-on: ubuntu-22.04 13 | steps: 14 | - name: Checkout Git repository 15 | uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 16 | 17 | - name: Cache pip 18 | uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 19 | with: 20 | path: ~/.cache/pip 21 | key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }} 22 | restore-keys: | 23 | ${{ runner.os }}-pip- 24 | 25 | - name: Set up Python 3.13 26 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 27 | with: 28 | python-version: 3.13 29 | 30 | - name: Install 31 | run: |- 32 | sudo pip3 install \ 33 | --disable-pip-version-check \ 34 | . 35 | 36 | - name: Install runtime dependencies 37 | run: |- 38 | sudo apt-get update 39 | sudo apt-get install --no-install-recommends --yes \ 40 | debootstrap \ 41 | extlinux \ 42 | kpartx \ 43 | mbr \ 44 | qemu-utils \ 45 | ubuntu-keyring 46 | 47 | - name: Smoke test creation of Ubuntu OpenStack images 48 | run: |- 49 | cd /tmp # to not be in Git clone folder 50 | 51 | image-bootstrap --help ; echo 52 | image-bootstrap ubuntu --help ; echo 53 | 54 | truncate --size 3g /tmp/disk 55 | LOOP_DEV="$(sudo losetup --show --find -f /tmp/disk | tee /dev/stderr)" 56 | echo "LOOP_DEV=${LOOP_DEV}" >> "${GITHUB_ENV}" 57 | 58 | sudo PYTHONUNBUFFERED=1 image-bootstrap --verbose --debug --openstack ubuntu ${LOOP_DEV} 59 | 60 | - name: Create .qcow2 image from loop device 61 | run: |- 62 | set -eux 63 | git fetch --force --tags --unshallow origin # for "git describe" 64 | img_base_name="ubuntu-openstack-$(date '+%Y-%m-%d-%H-%M')-image-bootstrap-$(git describe --tags).qcow2" 65 | sudo qemu-img convert -f raw -O qcow2 "${LOOP_DEV}" "${img_base_name}" 66 | ls -lh "${img_base_name}" 67 | 68 | - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 69 | with: 70 | name: ubuntu-openstack-qcow2 71 | path: '*.qcow2' 72 | if-no-files-found: error 73 | -------------------------------------------------------------------------------- /.github/workflows/run-test-suite.yml: -------------------------------------------------------------------------------- 1 | name: Run the test suite 2 | 3 | on: 4 | pull_request: 5 | push: 6 | schedule: 7 | - cron: '0 16 * * 5' # Every Friday 4pm 8 | workflow_dispatch: 9 | 10 | jobs: 11 | install_and_run: 12 | name: Run the test suite 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | python-version: [3.9, 3.13] # no need for anything in between 17 | runs-on: ubuntu-24.04 18 | steps: 19 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 20 | 21 | - name: Set up Python ${{ matrix.python-version }} 22 | uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 23 | with: 24 | python-version: ${{ matrix.python-version }} 25 | 26 | - name: Install test dependencies 27 | run: |- 28 | python3 -m venv venv/ 29 | source venv/bin/activate 30 | pip3 install \ 31 | --disable-pip-version-check \ 32 | --no-warn-script-location \ 33 | -r requirements.txt 34 | pip3 check 35 | # Ensure that even indirect dependencies are fully pinned 36 | diff -u0 \ 37 | <(sed -e '/^#/d' -e '/^$/d' requirements.txt | sort -f) \ 38 | <(pip3 freeze | sort -f) 39 | 40 | - name: Run the test suite 41 | run: |- 42 | source venv/bin/activate 43 | coverage run -m pytest -v --doctest-modules 44 | coverage report --show-missing | tee coverage.txt 45 | coverage html 46 | 47 | - name: Upload HTML coverage report as an artifact 48 | uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 49 | with: 50 | name: "coverage_python_${{ matrix.python-version }}" # .zip 51 | path: | 52 | coverage.txt 53 | htmlcov/ 54 | if-no-files-found: error 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /dist 3 | /MANIFEST 4 | *.pyc 5 | -------------------------------------------------------------------------------- /debian/.gitignore: -------------------------------------------------------------------------------- 1 | /files 2 | /image-bootstrap/ 3 | /image-bootstrap.debhelper.log 4 | /image-bootstrap.postinst.debhelper 5 | /image-bootstrap.prerm.debhelper 6 | /image-bootstrap.substvars 7 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | image-bootstrap (0.9.2.1) unstable; urgency=low 2 | 3 | * Initial Release (with help from stdeb and dh_make). 4 | 5 | -- Sebastian Pipping Tue, 10 Jan 2017 21:16:45 +0100 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: image-bootstrap 2 | Section: admin 3 | Priority: extra 4 | Maintainer: Sebastian Pipping 5 | Build-Depends: python-all (>= 2.6.6-3), debhelper (>= 8.0.0) 6 | Standards-Version: 3.9.3 7 | Homepage: https://github.com/hartwork/image-bootstrap 8 | 9 | Package: image-bootstrap 10 | Architecture: all 11 | Depends: ${misc:Depends}, ${python:Depends}, 12 | python-colorama, python-bs4, python-requests, python-pkg-resources, 13 | python-lxml, python-yaml, 14 | debootstrap | cdebootstrap | cdebootstrap-static, 15 | debian-archive-keyring, ubuntu-keyring | ubuntu-archive-keyring, gnupg, 16 | extlinux, mbr, 17 | grub-pc | grub-coreboot | grub-efi-amd64 | grub-efi-ia32 | grub-ieee1275 | grub-yeeloong, 18 | kpartx, 19 | parted 20 | Description: Command line tool for creating bootable virtual machine images 21 | Started as a replacement to grml-debootstrap. 22 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: image-bootstrap 3 | Source: https://github.com/hartwork/image-bootstrap 4 | 5 | Files: * 6 | Copyright: 2015 Sebastian Pipping 7 | License: AGPL-3.0+ 8 | 9 | Files: debian/* 10 | Copyright: 2015 Sebastian Pipping 11 | License: AGPL-3.0+ 12 | 13 | License: AGPL-3.0+ 14 | This program is free software: you can redistribute it and/or modify 15 | it under the terms of the GNU Affero General Public License as published by 16 | the Free Software Foundation, either version 3 of the License, or 17 | (at your option) any later version. 18 | . 19 | This package is distributed in the hope that it will be useful, 20 | but WITHOUT ANY WARRANTY; without even the implied warranty of 21 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 22 | GNU General Public License for more details. 23 | . 24 | You should have received a copy of the GNU Affero General Public License 25 | along with this program. If not, see . 26 | . 27 | On Debian systems, the complete text of the GNU Affero General 28 | Public License version 3 can be found in "/usr/share/common-licenses/AGPL-3". 29 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Copyright (C) 2015 Sebastian Pipping 4 | # Licensed under AGPL v3 or later 5 | 6 | export DH_VERBOSE=0 7 | 8 | %: 9 | dh $@ --with python3 --buildsystem=python_distutils 10 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (native) 2 | -------------------------------------------------------------------------------- /directory_bootstrap/.gitignore: -------------------------------------------------------------------------------- 1 | /dist 2 | /directory_bootstrap.egg-info/ 3 | -------------------------------------------------------------------------------- /directory_bootstrap/MANIFEST.in: -------------------------------------------------------------------------------- 1 | include setup-pypi-readme.rst 2 | -------------------------------------------------------------------------------- /directory_bootstrap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import signal 5 | import sys 6 | 7 | import directory_bootstrap.shared.loaders._argparse as argparse 8 | from directory_bootstrap.distros.alpine import AlpineBootstrapper 9 | from directory_bootstrap.distros.arch import ArchBootstrapper 10 | from directory_bootstrap.distros.base import ( 11 | BOOTSTRAPPER_CLASS_FIELD, add_general_directory_bootstrapping_options) 12 | from directory_bootstrap.distros.gentoo import GentooBootstrapper 13 | from directory_bootstrap.distros.void import VoidBootstrapper 14 | from directory_bootstrap.shared.executor import Executor, sanitize_path 15 | from directory_bootstrap.shared.messenger import (VERBOSITY_VERBOSE, Messenger, 16 | fix_output_encoding) 17 | from directory_bootstrap.shared.metadata import VERSION_STR 18 | from directory_bootstrap.shared.output_control import ( 19 | add_output_control_options, is_color_wanted, run_handle_errors) 20 | 21 | 22 | def _main__level_three(messenger, options): 23 | stdout_wanted = options.verbosity is VERBOSITY_VERBOSE 24 | 25 | if stdout_wanted: 26 | child_process_stdout = None 27 | else: 28 | child_process_stdout = open('/dev/null', 'w') 29 | 30 | sanitize_path() 31 | 32 | executor = Executor(messenger, stdout=child_process_stdout) 33 | 34 | 35 | bootstrapper_class = getattr(options, BOOTSTRAPPER_CLASS_FIELD) 36 | bootstrap = bootstrapper_class.create(messenger, executor, options) 37 | 38 | bootstrap.check_for_commands() 39 | if bootstrap.wants_to_be_unshared(): 40 | bootstrap.unshare() 41 | bootstrap.run() 42 | 43 | 44 | if not stdout_wanted: 45 | child_process_stdout.close() 46 | 47 | 48 | def _main__level_two(): 49 | parser = argparse.ArgumentParser(prog='directory-bootstrap') 50 | parser.add_argument('--version', action='version', version=VERSION_STR) 51 | 52 | add_output_control_options(parser) 53 | 54 | general = parser.add_argument_group('general configuration') 55 | add_general_directory_bootstrapping_options(general) 56 | 57 | system = parser.add_argument_group('system configuration') 58 | system.add_argument('--resolv-conf', metavar='FILE', default='/etc/resolv.conf', 59 | help='file to copy nameserver entries from (default: %(default)s)') 60 | 61 | distros = parser.add_subparsers(title='subcommands (choice of distribution)', 62 | description='Run "%(prog)s DISTRIBUTION --help" for details ' 63 | 'on options specific to that distribution.', 64 | metavar='DISTRIBUTION', help='choice of distribution, pick from:') 65 | 66 | 67 | for strategy_clazz in ( 68 | AlpineBootstrapper, 69 | ArchBootstrapper, 70 | GentooBootstrapper, 71 | VoidBootstrapper, 72 | ): 73 | strategy_clazz.add_parser_to(distros) 74 | 75 | 76 | parser.add_argument('target_dir', metavar='DIRECTORY') 77 | 78 | options = parser.parse_args() 79 | 80 | 81 | messenger = Messenger(options.verbosity, is_color_wanted(options)) 82 | run_handle_errors(_main__level_three, messenger, options) 83 | 84 | 85 | def main(): 86 | try: 87 | fix_output_encoding() 88 | _main__level_two() 89 | except KeyboardInterrupt: 90 | sys.exit(128 + signal.SIGINT) 91 | 92 | 93 | if __name__ == '__main__': 94 | main() 95 | -------------------------------------------------------------------------------- /directory_bootstrap/distros/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/distros/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/distros/alpine.py: -------------------------------------------------------------------------------- 1 | import importlib.resources 2 | import os 3 | import re 4 | import shutil 5 | import tempfile 6 | from tarfile import TarFile 7 | 8 | import directory_bootstrap.resources.alpine as resources 9 | from directory_bootstrap.distros.base import DirectoryBootstrapper 10 | from directory_bootstrap.shared.commands import COMMAND_GPG, COMMAND_UNSHARE 11 | 12 | 13 | SUPPORTED_ARCHITECTURES = ('i686', 'x86_64') 14 | 15 | 16 | class AlpineBootstrapper(DirectoryBootstrapper): 17 | DISTRO_KEY = 'alpine' 18 | DISTRO_NAME_LONG = 'Alpine Linux' 19 | 20 | __version_extractor = re.compile( 21 | 'Current Alpine Version (?P[0-9][^<]+)') 22 | 23 | class VersionException(Exception): 24 | pass 25 | 26 | def __init__(self, messenger, executor, abs_target_dir, abs_cache_dir, 27 | architecture, 28 | abs_resolv_conf): 29 | super(AlpineBootstrapper, self).__init__( 30 | messenger, 31 | executor, 32 | abs_target_dir, 33 | abs_cache_dir, 34 | ) 35 | self._architecture = architecture 36 | self._abs_resolv_conf = abs_resolv_conf 37 | 38 | def wants_to_be_unshared(self): 39 | return True 40 | 41 | @staticmethod 42 | def get_commands_to_check_for(): 43 | return DirectoryBootstrapper.get_commands_to_check_for() + [ 44 | COMMAND_GPG, 45 | COMMAND_UNSHARE, 46 | ] 47 | 48 | def _determine_latest_version(self): 49 | downloads_page_html = self.get_url_content('https://alpinelinux.org/downloads/') 50 | 51 | match = self.__version_extractor.search(downloads_page_html) 52 | if match is None: 53 | raise VersionException('Could not determine latest release version.') 54 | 55 | return match.group('version') 56 | 57 | @staticmethod 58 | def _parse_version(version_str): 59 | version_tuple = version_str.split('.') 60 | if len(version_tuple) < 3: 61 | raise VersionException('Version "{}" has unsupported format'.format(version_str)) 62 | 63 | return version_tuple 64 | 65 | @staticmethod 66 | def _create_tarball_download_url(version_tuple, arch): 67 | return ('http://dl-cdn.alpinelinux.org/alpine/v{major}.{minor}/releases/{arch}/alpine-minirootfs-{major}.{minor}.{patch}-{arch}.tar.gz' 68 | .format(major=version_tuple[0], 69 | minor=version_tuple[1], 70 | patch=version_tuple[2], arch=arch)) 71 | 72 | def _download_file(self, url): 73 | basename = url.split('/')[-1] 74 | abs_filename = os.path.join(self._abs_cache_dir, basename) 75 | self.download_url_to_file(url, abs_filename) 76 | return abs_filename 77 | 78 | def run(self): 79 | self.ensure_directories_writable() 80 | 81 | self._messenger.info('Searching for latest release...') 82 | version_str = self._determine_latest_version() 83 | version_tuple = self._parse_version(version_str) 84 | self._messenger.info('Found {} to be latest.'.format(version_str)) 85 | 86 | tarball_download_url = self._create_tarball_download_url( 87 | version_tuple, self._architecture) 88 | signature_download_url = '{}.asc'.format(tarball_download_url) 89 | 90 | # Signature first, so we fail earlier if we do 91 | abs_filename_signature = self._download_file(signature_download_url) 92 | abs_filename_tarball = self._download_file(tarball_download_url) 93 | 94 | abs_temp_dir = os.path.abspath(tempfile.mkdtemp()) 95 | try: 96 | abs_gpg_home_dir = self._initialize_gpg_home(abs_temp_dir) 97 | release_pubring_gpg = str(importlib.resources 98 | .files(resources.__name__) 99 | .joinpath("ncopa.asc")) 100 | self._import_gpg_key_file(abs_gpg_home_dir, release_pubring_gpg) 101 | self._verify_file_gpg(abs_filename_tarball, 102 | abs_filename_signature, abs_gpg_home_dir) 103 | 104 | self._messenger.info('Extracting to "{}"...'.format(self._abs_target_dir)) 105 | with TarFile.open(abs_filename_tarball) as tf: 106 | tf.extractall(path=self._abs_target_dir) 107 | finally: 108 | self._messenger.info('Cleaning up "{}"...'.format(abs_temp_dir)) 109 | shutil.rmtree(abs_temp_dir) 110 | 111 | @classmethod 112 | def add_arguments_to(clazz, distro): 113 | distro.add_argument('--arch', dest='architecture', default='x86_64', 114 | choices=SUPPORTED_ARCHITECTURES, 115 | help='architecture (e.g. x86_64)') 116 | 117 | @classmethod 118 | def create(clazz, messenger, executor, options): 119 | return clazz( 120 | messenger, 121 | executor, 122 | os.path.abspath(options.target_dir), 123 | os.path.abspath(options.cache_dir), 124 | options.architecture, 125 | os.path.abspath(options.resolv_conf), 126 | ) 127 | -------------------------------------------------------------------------------- /directory_bootstrap/distros/arch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: UTF-8 -*- 2 | # Copyright (C) 2015 Sebastian Pipping 3 | # Licensed under AGPL v3 or later 4 | 5 | 6 | 7 | import datetime 8 | import os 9 | import re 10 | import shutil 11 | import tempfile 12 | from collections import namedtuple 13 | from tarfile import TarFile 14 | 15 | from directory_bootstrap.distros.base import ( 16 | DirectoryBootstrapper, date_argparse_type) 17 | from directory_bootstrap.shared.commands import ( 18 | COMMAND_CHROOT, COMMAND_MOUNT, COMMAND_TAR, 19 | COMMAND_UMOUNT, COMMAND_UNSHARE) 20 | from directory_bootstrap.shared.mount import try_unmounting 21 | from directory_bootstrap.shared.resolv_conf import filter_copy_resolv_conf 22 | 23 | SUPPORTED_ARCHITECTURES = ('i686', 'x86_64') 24 | 25 | _NON_DISK_MOUNT_TASKS = ( 26 | ('devtmpfs', ['-t', 'devtmpfs'], 'dev'), 27 | ('devpts', ['-t', 'devpts'], 'dev/pts'), # for gpgme 28 | ('proc', ['-t', 'proc'], 'proc'), # for pacstrap mountpoint detection 29 | ) 30 | 31 | 32 | _year = '([2-9][0-9]{3})' 33 | _month = '(0[1-9]|1[0-2])' 34 | _day = '(0[1-9]|[12][0-9]|3[01])' 35 | 36 | _keyring_package_date_matcher = re.compile('%s%s%s' % (_year, _month, _day)) 37 | _image_date_matcher = re.compile('%s\\.%s\\.%s' % (_year, _month, _day)) 38 | 39 | 40 | class ArchBootstrapper(DirectoryBootstrapper): 41 | DISTRO_KEY = 'arch' 42 | DISTRO_NAME_LONG = 'Arch Linux' 43 | 44 | def __init__(self, messenger, executor, abs_target_dir, abs_cache_dir, 45 | architecture, image_date_triple_or_none, mirror_url, 46 | abs_resolv_conf): 47 | super(ArchBootstrapper, self).__init__( 48 | messenger, 49 | executor, 50 | abs_target_dir, 51 | abs_cache_dir, 52 | ) 53 | self._architecture = architecture 54 | self._image_date_triple_or_none = image_date_triple_or_none 55 | self._mirror_url = mirror_url 56 | self._abs_resolv_conf = abs_resolv_conf 57 | 58 | def wants_to_be_unshared(self): 59 | return True 60 | 61 | @staticmethod 62 | def get_commands_to_check_for(): 63 | return DirectoryBootstrapper.get_commands_to_check_for() + [ 64 | COMMAND_CHROOT, 65 | COMMAND_MOUNT, 66 | COMMAND_TAR, 67 | COMMAND_UMOUNT, 68 | ] 69 | 70 | def _get_image_listing(self): 71 | self._messenger.info('Downloading image listing...') 72 | return self.get_url_content('https://mirrors.kernel.org/archlinux/iso/') 73 | 74 | def _download_image(self, image_yyyy_mm_dd, suffix=''): 75 | filename = os.path.join(self._abs_cache_dir, 'archlinux-bootstrap-%s-%s.tar.zst%s' % (image_yyyy_mm_dd, self._architecture, suffix)) 76 | url = 'https://mirrors.kernel.org/archlinux/iso/%s/archlinux-bootstrap-%s-%s.tar.zst%s' % (image_yyyy_mm_dd, image_yyyy_mm_dd, self._architecture, suffix) 77 | self.download_url_to_file(url, filename) 78 | return filename 79 | 80 | def _extract_image(self, image_filename, abs_temp_dir): 81 | abs_pacstrap_outer_root = os.path.join(abs_temp_dir, 'pacstrap_root', '') 82 | 83 | self._messenger.info('Extracting bootstrap image to "%s"...' % abs_pacstrap_outer_root) 84 | abs_pacstrap_inner_root = os.path.join(abs_pacstrap_outer_root, 'root.%s' % self._architecture) 85 | 86 | os.makedirs(abs_pacstrap_outer_root) 87 | self._executor.check_call([COMMAND_TAR, 88 | 'xf', image_filename, 89 | '-C', abs_pacstrap_outer_root, 90 | ]) 91 | 92 | return abs_pacstrap_inner_root 93 | 94 | def _make_chroot_env(self): 95 | env = os.environ.copy() 96 | for key in ('LANG', 'LANGUAGE'): 97 | env.pop(key, None) 98 | env.update({ 99 | 'LC_ALL': 'C', 100 | }) 101 | return env 102 | 103 | def _adjust_pacman_mirror_list(self, abs_pacstrap_inner_root): 104 | abs_mirrorlist = os.path.join(abs_pacstrap_inner_root, 'etc/pacman.d/mirrorlist') 105 | self._messenger.info('Adjusting mirror list at "%s"...' % abs_mirrorlist) 106 | with open(abs_mirrorlist, 'a') as f: 107 | print(file=f) 108 | print('## Added by directory-bootstrap', file=f) 109 | print('Server = %s' % self._mirror_url, file=f) 110 | 111 | def _copy_etc_resolv_conf(self, abs_pacstrap_inner_root): 112 | target = os.path.join(abs_pacstrap_inner_root, 'etc/resolv.conf') 113 | filter_copy_resolv_conf(self._messenger, self._abs_resolv_conf, target) 114 | 115 | def _initialize_pacman_keyring(self, abs_pacstrap_inner_root): 116 | self._messenger.info('Initializing pacman keyring... (may take 2 to 7 minutes)') 117 | before = datetime.datetime.now() 118 | 119 | env = self._make_chroot_env() 120 | 121 | cmd = [ 122 | COMMAND_UNSHARE, 123 | '--fork', '--pid', # to auto-kill started gpg-agent 124 | COMMAND_CHROOT, 125 | abs_pacstrap_inner_root, 126 | 'pacman-key', 127 | '--init', 128 | ] 129 | self._executor.check_call(cmd, env=env) 130 | 131 | cmd = [ 132 | COMMAND_UNSHARE, 133 | '--fork', '--pid', # to auto-kill started gpg-agent 134 | COMMAND_CHROOT, 135 | abs_pacstrap_inner_root, 136 | 'pacman-key', 137 | '--populate', 'archlinux', 138 | ] 139 | self._executor.check_call(cmd, env=env) 140 | 141 | after = datetime.datetime.now() 142 | self._messenger.info('Took %d seconds.' % (after - before).total_seconds()) 143 | 144 | def _sync_archlinux_keyring(self, abs_pacstrap_inner_root): 145 | # NOTE: Motivation is to evade pacman's inspection of two 146 | # non-existing mountpoints "/var/cache/pacman/pkg/" and "/" 147 | self._messenger.info('Disabling CheckSpace for chroot pacman...') 148 | env = self._make_chroot_env() 149 | cmd = [ 150 | COMMAND_CHROOT, 151 | abs_pacstrap_inner_root, 152 | 'sed', 153 | '-e', 154 | 's/^CheckSpace/#CheckSpace/', 155 | '-e', 156 | 's/^DownloadUser/#DownloadUser/', 157 | '-i', 158 | '/etc/pacman.conf', 159 | ] 160 | self._executor.check_call(cmd, env=env) 161 | 162 | self._messenger.info('Syncing package archlinux-keyring...') 163 | cmd = [ 164 | COMMAND_UNSHARE, 165 | '--fork', '--pid', # to auto-kill started gpg-agent 166 | COMMAND_CHROOT, 167 | abs_pacstrap_inner_root, 168 | 'pacman', 169 | '--sync', '--refresh', '--noconfirm', 170 | 'archlinux-keyring', 171 | ] 172 | self._executor.check_call(cmd, env=env) 173 | 174 | def _run_pacstrap(self, abs_pacstrap_inner_root, rel_pacstrap_target_dir): 175 | self._messenger.info('Pacstrapping into "%s"...' 176 | % (os.path.join(abs_pacstrap_inner_root, rel_pacstrap_target_dir))) 177 | env = self._make_chroot_env() 178 | cmd = [ 179 | COMMAND_CHROOT, 180 | abs_pacstrap_inner_root, 181 | 'pacstrap', 182 | os.path.join('/', rel_pacstrap_target_dir), 183 | ] 184 | self._executor.check_call(cmd, env=env) 185 | 186 | def _fix_root_login_at(self, abs_chroot_dir): 187 | abs_chroot_etc_shadown = os.path.join(abs_chroot_dir, 'etc', 'shadow') 188 | self._messenger.info('Securing root account at "%s"...' % abs_chroot_etc_shadown) 189 | env = self._make_chroot_env() 190 | cmd = [ 191 | COMMAND_CHROOT, 192 | abs_chroot_dir, 193 | 'usermod', '-p', '*', 'root', 194 | ] 195 | self._executor.check_call(cmd, env=env) 196 | 197 | def _mount_disk_chroot_mounts(self, abs_pacstrap_target_dir): 198 | self._executor.check_call([ 199 | COMMAND_MOUNT, 200 | '-o', 'bind', 201 | self._abs_target_dir, 202 | abs_pacstrap_target_dir, 203 | ]) 204 | 205 | def _mount_nondisk_chroot_mounts(self, abs_pacstrap_inner_root): 206 | self._messenger.info('Mounting non-disk file systems...') 207 | for source, options, target in _NON_DISK_MOUNT_TASKS: 208 | self._executor.check_call([ 209 | COMMAND_MOUNT, 210 | source, 211 | ] \ 212 | + options \ 213 | + [ 214 | os.path.join(abs_pacstrap_inner_root, target), 215 | ]) 216 | 217 | def _unmount_disk_chroot_mounts(self, abs_pacstrap_target_dir): 218 | try_unmounting(self._executor, abs_pacstrap_target_dir) 219 | 220 | def _unmount_nondisk_chroot_mounts(self, abs_pacstrap_inner_root): 221 | self._messenger.info('Unmounting non-disk file systems...') 222 | for source, options, target in reversed(_NON_DISK_MOUNT_TASKS): 223 | abs_path = os.path.join(abs_pacstrap_inner_root, target) 224 | try_unmounting(self._executor, abs_path) 225 | 226 | def run(self): 227 | self.ensure_directories_writable() 228 | 229 | abs_temp_dir = os.path.abspath(tempfile.mkdtemp()) 230 | try: 231 | if self._image_date_triple_or_none is None: 232 | image_listing_html = self._get_image_listing() 233 | image_yyyy_mm_dd = self.extract_latest_date(image_listing_html, _image_date_matcher) 234 | else: 235 | image_yyyy_mm_dd = '%04s.%02d.%02d' % self._image_date_triple_or_none 236 | 237 | image_filename = self._download_image(image_yyyy_mm_dd) 238 | 239 | abs_pacstrap_inner_root = self._extract_image(image_filename, abs_temp_dir) 240 | self._adjust_pacman_mirror_list(abs_pacstrap_inner_root) 241 | self._copy_etc_resolv_conf(abs_pacstrap_inner_root) 242 | 243 | 244 | rel_pacstrap_target_dir = os.path.join('mnt', 'arch_root', '') 245 | abs_pacstrap_target_dir = os.path.join(abs_pacstrap_inner_root, rel_pacstrap_target_dir) 246 | 247 | os.makedirs(abs_pacstrap_target_dir) 248 | 249 | self._mount_disk_chroot_mounts(abs_pacstrap_target_dir) 250 | try: 251 | self._mount_nondisk_chroot_mounts(abs_pacstrap_inner_root) 252 | try: 253 | self._initialize_pacman_keyring(abs_pacstrap_inner_root) 254 | self._sync_archlinux_keyring(abs_pacstrap_inner_root) 255 | self._run_pacstrap(abs_pacstrap_inner_root, rel_pacstrap_target_dir) 256 | self._fix_root_login_at(abs_pacstrap_target_dir) 257 | finally: 258 | self._unmount_nondisk_chroot_mounts(abs_pacstrap_inner_root) 259 | finally: 260 | self._unmount_disk_chroot_mounts(abs_pacstrap_target_dir) 261 | 262 | finally: 263 | self._messenger.info('Cleaning up "%s"...' % abs_temp_dir) 264 | shutil.rmtree(abs_temp_dir) 265 | 266 | @classmethod 267 | def add_arguments_to(clazz, distro): 268 | distro.add_argument('--arch', dest='architecture', default='x86_64', 269 | choices=SUPPORTED_ARCHITECTURES, 270 | help='architecture (e.g. x86_64)') 271 | distro.add_argument('--image-date', type=date_argparse_type, metavar='YYYY-MM-DD', 272 | help='date to use bootstrap image of (e.g. 2015-05-01, default: latest available)') 273 | distro.add_argument('--mirror', dest='mirror_url', metavar='URL', 274 | default='http://mirror.rackspace.com/archlinux/$repo/os/$arch', 275 | help='pacman mirror to use (default: %(default)s)') 276 | 277 | @classmethod 278 | def create(clazz, messenger, executor, options): 279 | return clazz( 280 | messenger, 281 | executor, 282 | os.path.abspath(options.target_dir), 283 | os.path.abspath(options.cache_dir), 284 | options.architecture, 285 | options.image_date, 286 | options.mirror_url, 287 | os.path.abspath(options.resolv_conf), 288 | ) 289 | -------------------------------------------------------------------------------- /directory_bootstrap/distros/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import errno 7 | import os 8 | import re 9 | from abc import ABCMeta, abstractmethod 10 | from textwrap import dedent 11 | 12 | import directory_bootstrap.shared.loaders._requests as requests 13 | from directory_bootstrap.shared.commands import ( 14 | COMMAND_GPG, COMMAND_WGET, COMMAND_UNSHARE, COMMAND_UNXZ, 15 | check_for_commands) 16 | from directory_bootstrap.shared.loaders._bs4 import BeautifulSoup 17 | from directory_bootstrap.shared.namespace import unshare_current_process 18 | 19 | BOOTSTRAPPER_CLASS_FIELD = 'bootstrapper_class' 20 | 21 | _year = '([2-9][0-9]{3})' 22 | _month = '(0[1-9]|1[0-2])' 23 | _day = '(0[1-9]|[12][0-9]|3[01])' 24 | 25 | _argparse_date_matcher = re.compile('^%s-%s-%s$' % (_year, _month, _day)) 26 | 27 | _GPG_DISPLAY_KEY_FORMAT = '0xlong' 28 | 29 | 30 | def date_argparse_type(text): 31 | m = _argparse_date_matcher.match(text) 32 | if m is None: 33 | raise ValueError('Not a well-formed date: "%s"' % text) 34 | return tuple((int(m.group(i)) for i in range(1, 3 + 1))) 35 | 36 | date_argparse_type.__name__ = 'date' 37 | 38 | 39 | def add_general_directory_bootstrapping_options(general): 40 | general.add_argument('--cache-dir', metavar='DIRECTORY', 41 | default='/var/cache/directory-bootstrap/', 42 | help='directory to use for downloads (default: %(default)s)') 43 | 44 | 45 | class DirectoryBootstrapper(object, metaclass=ABCMeta): 46 | def __init__(self, messenger, executor, abs_target_dir, abs_cache_dir): 47 | self._messenger = messenger 48 | self._executor = executor 49 | self._abs_target_dir = abs_target_dir 50 | self._abs_cache_dir = abs_cache_dir 51 | 52 | @abstractmethod 53 | def wants_to_be_unshared(self): 54 | pass 55 | 56 | @classmethod 57 | def add_parser_to(clazz, distros): 58 | distro = distros.add_parser(clazz.DISTRO_KEY, help=clazz.DISTRO_NAME_LONG) 59 | distro.set_defaults(**{BOOTSTRAPPER_CLASS_FIELD: clazz}) 60 | clazz.add_arguments_to(distro) 61 | 62 | def check_for_commands(self): 63 | check_for_commands(self._messenger, self.get_commands_to_check_for()) 64 | 65 | @staticmethod 66 | def get_commands_to_check_for(): 67 | return [ 68 | COMMAND_WGET, 69 | ] 70 | 71 | def unshare(self): 72 | unshare_current_process(self._messenger) 73 | 74 | def extract_latest_date(self, listing_html, date_matcher): 75 | soup = BeautifulSoup(listing_html, 'lxml') 76 | dates = [] 77 | for link in soup.find_all('a'): 78 | m = date_matcher.search(link.get('href')) 79 | if not m: 80 | continue 81 | dates.append(m.group(0)) 82 | 83 | return sorted(dates)[-1] 84 | 85 | @abstractmethod 86 | def run(self): 87 | pass 88 | 89 | @classmethod 90 | def add_arguments_to(clazz, distro): 91 | raise NotImplementedError() 92 | 93 | @classmethod 94 | def create(clazz, messenger, executor, options): 95 | raise NotImplementedError() 96 | 97 | def get_url_content(self, url): 98 | response = requests.get(url) 99 | response.raise_for_status() 100 | return response.text 101 | 102 | def download_url_to_file(self, url, filename): 103 | if os.path.exists(filename): 104 | self._messenger.info('Re-using cache file "%s".' % filename) 105 | return 106 | 107 | self._messenger.info('Downloading "%s"...' % url) 108 | cmd = [ 109 | COMMAND_WGET, 110 | '-O%s' % filename, 111 | url, 112 | ] 113 | self._executor.check_call(cmd) 114 | 115 | def uncompress_xz_tarball(self, tarball_filename): 116 | extension = '.xz' 117 | 118 | if not tarball_filename.endswith(extension): 119 | raise ValueError('Filename "%s" does not end with "%s"' % (tarball_filename, extension)) 120 | 121 | uncompressed_tarball_filename = tarball_filename[:-len(extension)] 122 | 123 | if os.path.exists(uncompressed_tarball_filename): 124 | self._messenger.info('Re-using cache file "%s".' % uncompressed_tarball_filename) 125 | else: 126 | self._messenger.info('Uncompressing file "%s"...' % tarball_filename) 127 | self._executor.check_call([ 128 | COMMAND_UNXZ, 129 | '--keep', 130 | tarball_filename, 131 | ]) 132 | 133 | if not os.path.exists(uncompressed_tarball_filename): 134 | raise OSError(errno.ENOENT, 'File "%s" does not exists' % uncompressed_tarball_filename) 135 | 136 | return uncompressed_tarball_filename 137 | 138 | def _ensure_directory_writable(self, abs_path, creation_mode): 139 | try: 140 | os.makedirs(abs_path, creation_mode) 141 | except OSError as e: 142 | if e.errno != errno.EEXIST: 143 | raise 144 | 145 | self._messenger.info('Checking access to "%s"...' % abs_path) 146 | if not os.path.exists(abs_path): 147 | raise IOError(errno.ENOENT, 'No such file or directory: \'%s\'' % abs_path) 148 | 149 | if not os.access(os.path.join(abs_path, ''), os.W_OK): 150 | raise IOError(errno.EACCES, 'Permission denied: \'%s\'' % abs_path) 151 | else: 152 | # NOTE: Sounding like future is intentional. 153 | self._messenger.info('Creating directory "%s"...' % abs_path) 154 | 155 | def ensure_directories_writable(self): 156 | self._ensure_directory_writable(self._abs_cache_dir, 0o755) 157 | self._ensure_directory_writable(self._abs_target_dir, 0o700) 158 | 159 | @staticmethod 160 | def _abs_keyserver_cert_filename(abs_gpg_home_dir): 161 | return os.path.join(abs_gpg_home_dir, 'sks-keyservers.netCA.pem') 162 | 163 | def _initialize_gpg_home(self, abs_temp_dir): 164 | abs_gpg_home_dir = os.path.join(abs_temp_dir, 'gpg_home') 165 | self._messenger.info('Initializing temporary GnuPG home at "%s"...' % abs_gpg_home_dir) 166 | os.mkdir(abs_gpg_home_dir, 0o700) 167 | 168 | self.download_url_to_file( 169 | # This one was trouble: https://sks-keyservers.net/sks-keyservers.netCA.pem 170 | 'https://raw.githubusercontent.com/gpg/gnupg/master/dirmngr/sks-keyservers.netCA.pem', 171 | self._abs_keyserver_cert_filename(abs_gpg_home_dir)) 172 | 173 | with open(os.path.join(abs_gpg_home_dir, 'dirmngr.conf'), 'w') as f: 174 | print(dedent("""\ 175 | keyserver hkps://hkps.pool.sks-keyservers.net 176 | hkp-cacert %s 177 | """ % self._abs_keyserver_cert_filename(abs_gpg_home_dir)), file=f) 178 | 179 | return abs_gpg_home_dir 180 | 181 | def _get_gpg_argv_start(self, abs_gpg_home_dir): 182 | return [ 183 | COMMAND_UNSHARE, 184 | '--fork', '--pid', # to auto-kill started gpg-agent 185 | COMMAND_GPG, 186 | '--home', abs_gpg_home_dir, 187 | '--keyid-format', _GPG_DISPLAY_KEY_FORMAT, 188 | '--batch', 189 | ] 190 | 191 | def _import_gpg_key_file(self, abs_gpg_home_dir, abs_key_path): 192 | self._messenger.info('Importing GPG key from file "{}"...'.format(abs_key_path)) 193 | cmd = self._get_gpg_argv_start(abs_gpg_home_dir) + [ 194 | '--quiet', 195 | '--import', abs_key_path, 196 | ] 197 | self._executor.check_call(cmd) 198 | 199 | def _verify_file_gpg(self, candidate_filename, signature_filename, abs_gpg_home_dir): 200 | self._messenger.info('Verifying integrity of file "%s"...' % candidate_filename) 201 | cmd = self._get_gpg_argv_start(abs_gpg_home_dir) + [ 202 | '--verify', 203 | signature_filename, 204 | candidate_filename, 205 | ] 206 | self._executor.check_call(cmd) 207 | -------------------------------------------------------------------------------- /directory_bootstrap/distros/gentoo.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import datetime 7 | import errno 8 | import importlib.resources 9 | import os 10 | import re 11 | import shutil 12 | import subprocess 13 | import tempfile 14 | import time 15 | 16 | import directory_bootstrap.resources.gentoo as resources 17 | import directory_bootstrap.shared.loaders._requests as requests 18 | from directory_bootstrap.distros.base import ( 19 | DirectoryBootstrapper, date_argparse_type) 20 | from directory_bootstrap.shared.commands import ( 21 | COMMAND_GPG, COMMAND_MD5SUM, COMMAND_SHA512SUM, COMMAND_TAR, 22 | COMMAND_UNXZ) 23 | from directory_bootstrap.tools.stage3_latest_parser import \ 24 | find_latest_stage3_date 25 | 26 | _GPG_DISPLAY_KEY_FORMAT = '0xlong' 27 | 28 | _year = '([2-9][0-9]{3})' 29 | _month = '(0[1-9]|1[0-2])' 30 | _day = '(0[1-9]|[12][0-9]|3[01])' 31 | 32 | _snapshot_date_matcher = re.compile('%s%s%s' % (_year, _month, _day)) 33 | 34 | 35 | class _ChecksumVerifiationFailed(Exception): 36 | def __init__(self, algorithm, filename): 37 | super(_ChecksumVerifiationFailed, self).__init__( 38 | 'File "%s" failed %s verification' \ 39 | % (filename, algorithm)) 40 | 41 | 42 | class _NotFreshEnoughException(Exception): 43 | def __init__(self, year_month_day_tuple, max_age_days): 44 | (year, month, day) = year_month_day_tuple 45 | super(_NotFreshEnoughException, self).__init__( 46 | '%04d-%02d-%02d was more than %d days ago, rejecting as too old' \ 47 | % (year, month, day, max_age_days)) 48 | 49 | 50 | class GentooBootstrapper(DirectoryBootstrapper): 51 | DISTRO_KEY = 'gentoo' 52 | DISTRO_NAME_LONG = 'Gentoo' 53 | 54 | _MIRROR_BLACKLIST = set(( 55 | # All previous entries removed 56 | )) 57 | 58 | def __init__(self, messenger, executor, abs_target_dir, abs_cache_dir, 59 | architecture, mirror_url, max_age_days, 60 | stage3_date_triple_or_none, repository_date_triple_or_none, 61 | abs_resolv_conf): 62 | super(GentooBootstrapper, self).__init__( 63 | messenger, 64 | executor, 65 | abs_target_dir, 66 | abs_cache_dir, 67 | ) 68 | self._architecture = architecture 69 | self._architecture_family = self._extract_architecture_family(architecture) 70 | self._mirror_base_url = (mirror_url 71 | if mirror_url 72 | else self._retrieve_bounced_mirror_base_url() 73 | ).rstrip('/') 74 | self._max_age_days = max_age_days 75 | self._stage3_date_triple_or_none = stage3_date_triple_or_none 76 | self._repository_date_triple_or_none = repository_date_triple_or_none 77 | self._abs_resolv_conf = abs_resolv_conf 78 | 79 | self._gpg_supports_no_autostart = None 80 | 81 | def _retrieve_bounced_mirror_base_url(self): 82 | self._messenger.info('Obtaining mirror URL from bouncer.gentoo.org...') 83 | tries = 10 84 | for i in range(tries): 85 | response = requests.get('https://bouncer.gentoo.org/fetch/root/all/') 86 | response.raise_for_status() 87 | mirror_url = response.url.rstrip('/') 88 | 89 | if mirror_url not in self._MIRROR_BLACKLIST: 90 | break 91 | 92 | time.sleep(0.25) # to reduce server load 93 | 94 | self._messenger.info(f'Selected mirror {mirror_url} .') 95 | return mirror_url 96 | 97 | @staticmethod 98 | def _extract_architecture_family(architecture): 99 | """ 100 | Map "arm64", "armv6j" etc to arm 101 | """ 102 | if architecture.startswith('arm'): 103 | return 'arm' 104 | return architecture 105 | 106 | def wants_to_be_unshared(self): 107 | return False 108 | 109 | @staticmethod 110 | def get_commands_to_check_for(): 111 | return DirectoryBootstrapper.get_commands_to_check_for() + [ 112 | COMMAND_GPG, 113 | COMMAND_MD5SUM, 114 | COMMAND_SHA512SUM, 115 | COMMAND_TAR, 116 | COMMAND_UNXZ, 117 | ] 118 | 119 | def _get_stage3_latest_file_url(self): 120 | return '%s/releases/%s/autobuilds/latest-stage3.txt' % ( 121 | self._mirror_base_url, 122 | self._architecture_family, 123 | ) 124 | 125 | def _get_old_portage_snapshot_listing_url(self): 126 | return '%s/releases/snapshots/current/' % self._mirror_base_url 127 | 128 | def _get_new_portage_snapshot_listing_url(self): 129 | return '%s/snapshots/' % self._mirror_base_url 130 | 131 | def _find_latest_snapshot_date(self, snapshot_listing): 132 | return self.extract_latest_date(snapshot_listing, _snapshot_date_matcher) 133 | 134 | def _download_stage3(self, stage3_date_str, arch_flavor): 135 | res = [None, None] 136 | for target_index, basename in ( 137 | (1, 'stage3-%s%s-%s.tar.xz.DIGESTS' % (self._architecture, arch_flavor, stage3_date_str)), 138 | (0, 'stage3-%s%s-%s.tar.xz' % (self._architecture, arch_flavor, stage3_date_str)), 139 | ): 140 | filename = os.path.join(self._abs_cache_dir, basename) 141 | url = '%s/releases/%s/autobuilds/%s/%s' \ 142 | % (self._mirror_base_url, self._architecture_family, stage3_date_str, basename) 143 | self.download_url_to_file(url, filename) 144 | 145 | assert res[target_index] is None 146 | res[target_index] = filename 147 | 148 | return res 149 | 150 | def _download_snapshot(self, snapshot_date_str, snapshot_listing_url): 151 | res = [None, None, None, None] 152 | for target_index, basename in ( 153 | (1, 'portage-%s.tar.xz.gpgsig' % snapshot_date_str), 154 | (2, 'portage-%s.tar.xz.md5sum' % snapshot_date_str), 155 | (3, 'portage-%s.tar.xz.umd5sum' % snapshot_date_str), 156 | (0, 'portage-%s.tar.xz' % snapshot_date_str), 157 | ): 158 | filename = os.path.join(self._abs_cache_dir, basename) 159 | url = snapshot_listing_url + basename 160 | self.download_url_to_file(url, filename) 161 | 162 | assert res[target_index] is None 163 | res[target_index] = filename 164 | 165 | return res 166 | 167 | def _verify_sha512_sum(self, testee_file, digests_file): 168 | self._messenger.info('Verifying SHA512 checksum of file "%s"...' \ 169 | % testee_file) 170 | 171 | expected_sha512sum = None 172 | testee_file_basename = os.path.basename(testee_file) 173 | with open(digests_file, 'r') as f: 174 | upcoming_sha512 = False 175 | for l in f: 176 | line = l.rstrip() 177 | if upcoming_sha512: 178 | sha512, basename = line.split(' ') 179 | if basename == testee_file_basename: 180 | if expected_sha512sum is None: 181 | expected_sha512sum = sha512 182 | else: 183 | raise ValueError('File "%s" mentions "%s" multiple times' \ 184 | % (digests_file, testee_file_basename)) 185 | 186 | upcoming_sha512 = line == '# SHA512 HASH' 187 | 188 | if expected_sha512sum is None: 189 | raise ValueError('File "%s" does not mention "%s"' \ 190 | % (digests_file, testee_file_basename)) 191 | 192 | expected_sha512sum_output = '%s %s\n' % (expected_sha512sum, testee_file) 193 | sha512sum_output = self._executor.check_output([ 194 | COMMAND_SHA512SUM, 195 | testee_file, 196 | ]) 197 | 198 | if sha512sum_output != expected_sha512sum_output.encode('utf-8'): 199 | raise _ChecksumVerifiationFailed('SHA512', testee_file) 200 | 201 | def _verify_md5_sum(self, snapshot_tarball, snapshot_md5sum): 202 | self._messenger.info('Verifying MD5 checksum of file "%s"...' \ 203 | % snapshot_tarball) 204 | 205 | snapshot_tarball_basename = os.path.basename(snapshot_tarball) 206 | needle = snapshot_tarball_basename + '\n' 207 | with open(snapshot_md5sum, 'r') as f: 208 | if f.read().count(needle) != 1: 209 | raise ValueError('File "%s" does not mention "%s" exactly once' \ 210 | % (snapshot_md5sum, snapshot_tarball_basename)) 211 | 212 | cwd = os.path.dirname(snapshot_md5sum) 213 | self._executor.check_call([ 214 | COMMAND_MD5SUM, 215 | '--strict', 216 | '--check', 217 | snapshot_md5sum, 218 | ], cwd=cwd) 219 | 220 | def _extract_tarball(self, tarball_filename, abs_target_root): 221 | self._messenger.info('Extracting file "%s" to "%s"...' % (tarball_filename, abs_target_root)) 222 | self._executor.check_call([ 223 | COMMAND_TAR, 224 | 'xpf', 225 | tarball_filename, 226 | ], cwd=abs_target_root) 227 | 228 | def _require_fresh_enough(self, year_month_day_tuple): 229 | (year, month, day) = year_month_day_tuple 230 | date_to_check = datetime.date(year, month, day) 231 | today = datetime.date.today() 232 | if (today - date_to_check).days > self._max_age_days: 233 | raise _NotFreshEnoughException((year, month, day), self._max_age_days) 234 | 235 | def _format_date_stage3_tarball_filename(self, stage3_date_triple, stage3_date_extra=''): 236 | return '%04d%02d%02d%s' % tuple(stage3_date_triple + (stage3_date_extra,)) 237 | 238 | def _parse_snapshot_listing_date(self, snapshot_date_str): 239 | m = _snapshot_date_matcher.match(snapshot_date_str) 240 | return (int(m.group(1)), int(m.group(2)), int(m.group(3))) 241 | 242 | def _get_gpg_argv_start(self, abs_gpg_home_dir): 243 | assert self._gpg_supports_no_autostart is not None 244 | 245 | res = [ 246 | COMMAND_GPG, 247 | '--home', abs_gpg_home_dir, 248 | '--keyid-format', _GPG_DISPLAY_KEY_FORMAT, 249 | '--batch', 250 | ] 251 | 252 | if self._gpg_supports_no_autostart: 253 | res += [ 254 | '--no-autostart', 255 | ] 256 | 257 | return res 258 | 259 | def _check_gpg_for_no_autostart_support(self, abs_gpg_home_dir): 260 | self._messenger.info('Checking if GnuPG understands the --no-autostart option...') 261 | cmd_prefix = [ 262 | COMMAND_GPG, 263 | '--home', abs_gpg_home_dir, 264 | '--list-keys', 265 | ] 266 | 267 | try: 268 | self._executor.check_call(cmd_prefix + ['--no-autostart']) 269 | except subprocess.CalledProcessError: 270 | # Does it work without it, at least or is there some unrelated trouble? 271 | self._executor.check_call(cmd_prefix) 272 | 273 | self._gpg_supports_no_autostart = False 274 | self._messenger.info('No, it does not.') 275 | else: 276 | self._gpg_supports_no_autostart = True 277 | self._messenger.info('Yes, it does.') 278 | 279 | def _initialize_gpg_home(self, abs_temp_dir): 280 | abs_gpg_home_dir = os.path.join(abs_temp_dir, 'gpg_home') 281 | 282 | self._messenger.info('Initializing temporary GnuPG home at "%s"...' % abs_gpg_home_dir) 283 | os.mkdir(abs_gpg_home_dir, 0o700) 284 | 285 | self._check_gpg_for_no_autostart_support(abs_gpg_home_dir) 286 | 287 | self._messenger.info('Importing known GnuPG keys from disk...') 288 | signatures = [ # from https://www.gentoo.org/downloads/signatures/ 289 | # Key Fingerprint # Description # Created # Expiry 290 | ('13EBBDBEDE7A12775DFDB1BABB572E0E2D182910', 'Gentoo Linux Release Engineering (Automated Weekly Release Key)', '2009-08-25', '2020-07-01'), 291 | ('DCD05B71EAB94199527F44ACDB6B8C1F96D8BF6D', 'Gentoo ebuild repository signing key (Automated Signing Key)', '2011-11-25', '2020-07-01'), 292 | ('EF9538C9E8E64311A52CDEDFA13D0EF1914E7A72', 'Gentoo repository mirrors (automated git signing key)', '2018-05-28', '2020-07-01'), 293 | ('D99EAC7379A850BCE47DA5F29E6438C817072058', 'Gentoo Linux Release Engineering (Gentoo Linux Release Signing Key)', '2004-07-20', '2020-07-01'), 294 | ('ABD00913019D6354BA1D9A132839FE0D796198B1', 'Gentoo Authority Key L1', '2019-04-01', '2020-07-01'), 295 | ('18F703D702B1B9591373148C55D3238EC050396E', 'Gentoo Authority Key L2 for Services', '2019-04-01', '2020-07-01'), 296 | ('2C13823B8237310FA213034930D132FF0FF50EEB', 'Gentoo Authority Key L2 for Developers', '2019-04-01', '2020-07-01'), 297 | ] 298 | for signature in signatures: 299 | filename = str(importlib.resources 300 | .files(resources.__name__) 301 | .joinpath('{}.asc'.format(signature[0]))) 302 | cmd = self._get_gpg_argv_start(abs_gpg_home_dir) + [ 303 | '--import', filename, 304 | ] 305 | self._executor.check_call(cmd) 306 | 307 | return abs_gpg_home_dir 308 | 309 | def _verify_detachted_gpg_signature(self, candidate_filename, signature_filename, abs_gpg_home_dir): 310 | self._messenger.info('Verifying GnuPG signature of file "%s"...' % candidate_filename) 311 | cmd = self._get_gpg_argv_start(abs_gpg_home_dir) + [ 312 | '--verify', 313 | signature_filename, 314 | candidate_filename, 315 | ] 316 | self._executor.check_call(cmd) 317 | 318 | def _verify_clearsigned_gpg_signature(self, clearsigned_filename, output_filename, abs_gpg_home_dir): 319 | self._messenger.info('Verifying GnuPG signature of file "%s", writing file "%s"...' \ 320 | % (clearsigned_filename, output_filename)) 321 | 322 | if os.path.exists(output_filename): 323 | raise OSError(errno.EEXIST, 'File "%s" exists' % output_filename) 324 | 325 | cmd = self._get_gpg_argv_start(abs_gpg_home_dir) + [ 326 | '--output', output_filename, 327 | '--decrypt', clearsigned_filename, 328 | ] 329 | self._executor.check_call(cmd) 330 | 331 | if not os.path.exists(output_filename): 332 | raise OSError(errno.ENOENT, 'File "%s" does not exists' % output_filename) 333 | 334 | def run(self): 335 | self.ensure_directories_writable() 336 | 337 | abs_temp_dir = os.path.abspath(tempfile.mkdtemp()) 338 | try: 339 | abs_gpg_home_dir = self._initialize_gpg_home(abs_temp_dir) 340 | 341 | if self._stage3_date_triple_or_none is None: 342 | self._messenger.info('Searching for available stage3 tarballs...') 343 | stage3_latest_file_url = self._get_stage3_latest_file_url() 344 | stage3_latest_file_content = self.get_url_content(stage3_latest_file_url) 345 | stage3_date_triple, stage3_date_extra, stage3_flavor = find_latest_stage3_date(stage3_latest_file_content, stage3_latest_file_url, self._architecture) 346 | stage3_date_str = self._format_date_stage3_tarball_filename(stage3_date_triple, stage3_date_extra) 347 | self._messenger.info('Found "%s" to be latest.' % stage3_date_str) 348 | self._require_fresh_enough(stage3_date_triple) 349 | else: 350 | stage3_date_str = self._format_date_stage3_tarball_filename(self._stage3_date_triple_or_none, '') 351 | stage3_flavor = '' 352 | 353 | if self._repository_date_triple_or_none is None: 354 | self._messenger.info('Searching for available portage repository snapshots...') 355 | try: 356 | snapshot_listing_url = self._get_old_portage_snapshot_listing_url() 357 | snapshot_listing = self.get_url_content(snapshot_listing_url) 358 | except requests.exceptions.HTTPError: 359 | snapshot_listing_url = self._get_new_portage_snapshot_listing_url() 360 | snapshot_listing = self.get_url_content(snapshot_listing_url) 361 | snapshot_date_str = self._find_latest_snapshot_date(snapshot_listing) 362 | self._messenger.info('Found "%s" to be latest.' % snapshot_date_str) 363 | self._require_fresh_enough(self._parse_snapshot_listing_date(snapshot_date_str)) 364 | else: 365 | snapshot_date_str = '%04d%02d%02d' % self._repository_date_triple_or_none 366 | 367 | self._messenger.info('Downloading portage repository snapshot...') 368 | snapshot_tarball, snapshot_gpgsig, snapshot_md5sum, snapshot_uncompressed_md5sum \ 369 | = self._download_snapshot(snapshot_date_str, snapshot_listing_url) 370 | self._verify_detachted_gpg_signature(snapshot_tarball, snapshot_gpgsig, abs_gpg_home_dir) 371 | self._verify_md5_sum(snapshot_tarball, snapshot_md5sum) 372 | 373 | self._messenger.info('Downloading stage3 tarball...') 374 | stage3_tarball, stage3_digests_asc \ 375 | = self._download_stage3(stage3_date_str, arch_flavor=stage3_flavor) 376 | stage3_digests = os.path.join(abs_temp_dir, os.path.basename(stage3_digests_asc)[:-len('.asc')]) 377 | self._verify_clearsigned_gpg_signature(stage3_digests_asc, stage3_digests, abs_gpg_home_dir) 378 | self._verify_sha512_sum(stage3_tarball, stage3_digests) 379 | 380 | snapshot_tarball_uncompressed = self.uncompress_xz_tarball(snapshot_tarball) 381 | self._verify_md5_sum(snapshot_tarball_uncompressed, snapshot_uncompressed_md5sum) 382 | 383 | self._extract_tarball(stage3_tarball, self._abs_target_dir) 384 | abs_var_db_repos = os.path.join(self._abs_target_dir, 'var', 'db', 'repos') 385 | self._extract_tarball(snapshot_tarball_uncompressed, abs_var_db_repos) 386 | os.rename(os.path.join(abs_var_db_repos, 'portage'), os.path.join(abs_var_db_repos, 'gentoo')) 387 | finally: 388 | self._messenger.info('Cleaning up "%s"...' % abs_temp_dir) 389 | shutil.rmtree(abs_temp_dir) 390 | 391 | @classmethod 392 | def add_arguments_to(clazz, distro): 393 | distro.add_argument('--arch', dest='architecture', default='amd64', 394 | help='architecture (e.g. amd64)') 395 | distro.add_argument('--stage3-date', type=date_argparse_type, metavar='YYYY-MM-DD', 396 | help='date to use stage3 of (e.g. 2015-05-01, default: latest available)') 397 | distro.add_argument('--repository-date', type=date_argparse_type, metavar='YYYY-MM-DD', 398 | help='date to use portage repository snapshot of (e.g. 2015-05-01, default: latest available)') 399 | distro.add_argument('--max-age-days', type=int, metavar='DAYS', default=14, 400 | help='age in days to tolerate as recent enough (security feature, default: %(default)s days)') 401 | distro.add_argument('--mirror', dest='mirror_url', metavar='URL', 402 | help='precise mirror URL to use (default: let bouncer.gentoo.org decide)') 403 | 404 | @classmethod 405 | def create(clazz, messenger, executor, options): 406 | return clazz( 407 | messenger, 408 | executor, 409 | os.path.abspath(options.target_dir), 410 | os.path.abspath(options.cache_dir), 411 | options.architecture, 412 | options.mirror_url, 413 | options.max_age_days, 414 | options.stage3_date, 415 | options.repository_date, 416 | os.path.abspath(options.resolv_conf), 417 | ) 418 | -------------------------------------------------------------------------------- /directory_bootstrap/distros/void.py: -------------------------------------------------------------------------------- 1 | import errno 2 | import os 3 | import shutil 4 | import tempfile 5 | from tarfile import TarFile 6 | 7 | from directory_bootstrap.distros.base import DirectoryBootstrapper 8 | from directory_bootstrap.shared.commands import ( 9 | COMMAND_CP, 10 | COMMAND_TAR, 11 | COMMAND_UNXZ, 12 | ) 13 | 14 | 15 | SUPPORTED_ARCHITECTURES = ('i686', 'x86_64') 16 | 17 | 18 | class VoidBootstrapper(DirectoryBootstrapper): 19 | DISTRO_KEY = 'void' 20 | DISTRO_NAME_LONG = 'Void Linux' 21 | 22 | def __init__(self, messenger, executor, abs_target_dir, abs_cache_dir, 23 | architecture, 24 | abs_resolv_conf): 25 | super(VoidBootstrapper, self).__init__( 26 | messenger, 27 | executor, 28 | abs_target_dir, 29 | abs_cache_dir, 30 | ) 31 | self._architecture = architecture 32 | self._abs_resolv_conf = abs_resolv_conf 33 | 34 | def wants_to_be_unshared(self): 35 | return True 36 | 37 | @staticmethod 38 | def get_commands_to_check_for(): 39 | return DirectoryBootstrapper.get_commands_to_check_for() + [ 40 | COMMAND_CP, 41 | COMMAND_TAR, 42 | COMMAND_UNXZ, 43 | ] 44 | 45 | def _download_static_image(self): 46 | basename = 'xbps-static-latest.%s-musl.tar.xz' % self._architecture 47 | url = 'https://repo-default.voidlinux.org/static/%s' % basename 48 | abs_filename = os.path.join(self._abs_cache_dir, basename) 49 | self.download_url_to_file(url, abs_filename) 50 | return self.uncompress_xz_tarball(abs_filename) 51 | 52 | def _copy_keys_into_chroot(self, abs_temp_dir): 53 | rel_xbps_keys_path = 'var/db/xbps/keys' 54 | abs_target_xbps_keys_path = os.path.join(self._abs_target_dir, rel_xbps_keys_path) 55 | 56 | self._messenger.info('Copying xbps keys to "%s"...' % abs_target_xbps_keys_path) 57 | try: 58 | os.makedirs(abs_target_xbps_keys_path, 0o755) 59 | except OSError as e: 60 | if e.errno != errno.EEXIST: 61 | raise 62 | 63 | self._executor.check_call([ 64 | COMMAND_CP, 65 | '-r', 66 | os.path.join(abs_temp_dir, rel_xbps_keys_path), 67 | os.path.dirname(abs_target_xbps_keys_path), 68 | ]) 69 | 70 | def run(self): 71 | self.ensure_directories_writable() 72 | 73 | abs_temp_dir = os.path.abspath(tempfile.mkdtemp()) 74 | try: 75 | abs_static_image_filename = self._download_static_image() 76 | with TarFile.open(abs_static_image_filename) as tf: 77 | tf.extractall(path=abs_temp_dir) 78 | 79 | self._copy_keys_into_chroot(abs_temp_dir) 80 | 81 | self._messenger.info('Installing into "%s"...' % self._abs_target_dir) 82 | xbps_install = os.path.join(abs_temp_dir, 'usr/bin/xbps-install.static') 83 | self._executor.check_call([ 84 | xbps_install, 85 | '--rootdir', self._abs_target_dir, 86 | '--repository=https://repo-default.voidlinux.org/current/musl', 87 | '--sync', '--yes', 88 | 'base-system', 89 | ], cwd=abs_temp_dir) 90 | finally: 91 | self._messenger.info('Cleaning up "%s"...' % abs_temp_dir) 92 | shutil.rmtree(abs_temp_dir) 93 | 94 | @classmethod 95 | def add_arguments_to(clazz, distro): 96 | distro.add_argument('--arch', dest='architecture', default='x86_64', 97 | choices=SUPPORTED_ARCHITECTURES, 98 | help='architecture (e.g. x86_64)') 99 | 100 | @classmethod 101 | def create(clazz, messenger, executor, options): 102 | return clazz( 103 | messenger, 104 | executor, 105 | os.path.abspath(options.target_dir), 106 | os.path.abspath(options.cache_dir), 107 | options.architecture, 108 | os.path.abspath(options.resolv_conf), 109 | ) 110 | -------------------------------------------------------------------------------- /directory_bootstrap/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/resources/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/resources/alpine/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/resources/alpine/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/resources/alpine/ncopa.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | Version: GnuPG v2 3 | 4 | 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 50 | Mkc= 51 | =FcYC 52 | -----END PGP PUBLIC KEY BLOCK----- 53 | -------------------------------------------------------------------------------- /directory_bootstrap/resources/gentoo/13EBBDBEDE7A12775DFDB1BABB572E0E2D182910.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQINBEqUWzgBEACXftaG+HVuSQBEqdpBIg2SOVgWW/KbCihO5wPOsdbM93e+psmb 4 | wvw+OtNHADQvxocKMuZX8Q/j5i3nQ/ikQFW5Oj6UXvl1qyxZhR2P7GZSNQxn0eMI 5 | zAX08o691ws2/dFGXKmNT6btYJ0FxuTtTVSK6zi68WF+ILGK/O2TZXK9EKfZKPDH 6 | KHcGrUq4c03vcGANz/8ksJj2ZYEGxMr1h7Wfe9PVcm0gCB1MhYHNR755M47V5Pch 7 | fyxbs6vaKz82PgrNjjjbT0PISvnKReUOdA2PFUWry6UKQkiVrLVDRkd8fryLL8ey 8 | 5JxgVoJZ4echoVWQ0JYJ5lJTWmcZyxQYSAbz2w9dLB+dPyyGpyPp1KX1ADukbROp 9 | 9S11I9+oVnyGdUBm+AUme0ecekWvt4qiCw3azghLSwEyGZc4a8nNcwSqFmH7Rqdd 10 | 1+gHc+4tu4WHmguhMviifGXKyWiRERULp0obV33JEo/c4uwyAZHBTJtKtVaLb92z 11 | aRdh1yox2I85iumyt62lq9dfOuet4NNVDnUkqMYCQD23JB8IM+qnVaDwJ6oCSIKi 12 | nY3uyoqbVE6Lkm+Hk5q5pbvg1cpEH6HWWAl20EMCzMOoMcH0tPyQLDlD2Mml7veG 13 | kwdy3S6RkjCympbNzqWec2+hkU2c93Bgpfh7QP0GDN0qrzcNNFmrD5795QARAQAB 14 | tFNHZW50b28gTGludXggUmVsZWFzZSBFbmdpbmVlcmluZyAoQXV0b21hdGVkIFdl 15 | ZWtseSBSZWxlYXNlIEtleSkgPHJlbGVuZ0BnZW50b28ub3JnPoheBBARCAAGBQJR 16 | yaWCAAoJELP3uHLZaXat08sA/3paxuDydIV/8qe9PzgID6zifip9T9XfTDCRbHQR 17 | Kw0xAP9vTE9yoPuNMrF55AP9+68FbYaIO0sUxNN9CVby28iU7IkBHAQQAQIABgUC 18 | Ut9qEwAKCRAvJnjSPF2apBtSCACIPmfvwiBluwx1dz4/C4pUSIZmaRk5NrKhuADL 19 | CBUyH4X/Ajz4MhvXjAYeWpvtzxHW5sJL0mnNBQtkEM3SPGrYsJLCmhp0hW0lfYtq 20 | pG8Kymej7N7CJMYKW65HTvlLyCM1JpBy3OAXBgtxNIeho+dXbXTBPAUCje2MVS8h 21 | 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 145 | SzHiRfwW1RnOb+3fw0ffvrbV7Qr6axlnpF4RQaCXfpDAde8AzIwOGJJUcImdjd4Z 146 | 2Ji33vmhx3drD2SyYiZ3/3cPjheHBJgHp/LnOFb/ejlvaqIYpFKlIbqsPbl541mh 147 | jTsu2yrgfAlRBAIer9m6c0dtjIJnqnXByNhv11XdiH3rIURCJcKkZIBgMcosun3b 148 | MK9hHYQ+A2dvLbWIG4W67xDufYpzmRx8pLepfQa6FHgD/5a1AJ93fQrkkd0gaDbN 149 | t0H/DvQZqLaxaZHKtCVO7JZwPvxw4ttSjq6MW8v78I1D9i/WC53rvUeFTiChm7xt 150 | CMzXr08g51Xu/t3q0oOtaHhCHYGOiQI7BBMBAgAlBQJKlFs5AhsDBQkHhM4ABgsJ 151 | CAcDAgQVAggDAxYCAQIeAQIXgAAKCRC7Vy4OLRgpEBavEACN6PjhMdn3gv9Ee6s1 152 | 3ugDZARHVwxiBS2IIFarTj0tvwtX2GHme8u0AgQ8u1hhGIp/JvnUi8LylTTnxyoW 153 | eTS7kU7Lwv08pX0uI6MrGfTblRQoKWZQKGcTZ3b3Qj2l9EXUHIA0ZBj/RPPaqq4J 154 | QAjiQ3Ng2hCitpUNuMCvcMJtJSgBzbN3eas/U0Nj6KQR//////////////////// 155 | //////////////////////////////////////////////////////////////// 156 | //////////////////////////////////////////////////////////////// 157 | //////////////////////////////////////////////////////////////// 158 | //////////////////////////////////////////////////////////////// 159 | //////////////////////////////////////////////////////////////// 160 | //////////////////////////////////////////////////////////////// 161 | //////////////////////////////////////////////////////////////// 162 | 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 283 | =T+GL 284 | -----END PGP PUBLIC KEY BLOCK----- 285 | -------------------------------------------------------------------------------- /directory_bootstrap/resources/gentoo/18F703D702B1B9591373148C55D3238EC050396E.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBFyh9D8BCADFgG+gd9vTGxCIbw7hVtaY5cNNI/JN88etWo3Uu/NEA2Y5F/yN 4 | S01pZ7PfjAMePY5OmYgU5YGGCmgY2+vLbvOenY91eEdYVoi/gm8v9iAoKI9/pNlc 5 | A/DG3Iz5L7FJhxtN5R8voBfmxKjF2kykk0bOt7BxE0f0BmItIwnicR4z1TLZB8RS 6 | f238ahRQURJDbPlLNABkepSAHmOVuZWiPMx39l37sjs9ukO9JLPBK+JusoOk/Y4F 7 | Tre9MWy38ahEsdftaXW3frS26EVrZQ/sc5aQAW5IqT74y+pK45ji8gsGPKRBl/cS 8 | F4mx7aZEBZps4XbwisXbATjq8Y4W8yexAAvzABEBAAG0RUdlbnRvbyBBdXRob3Jp 9 | dHkgS2V5IEwyIGZvciBTZXJ2aWNlcyA8b3BlbnBncC1hdXRoK2wyLXNydkBnZW50 10 | b28ub3JnPokBTAQTAQoANgIbAQULCQoNBAMVCggCHgECF4AWIQQY9wPXArG5WRNz 11 | FIxV0yOOwFA5bgUCXMRsMAUJAlpqYQAKCRBV0yOOwFA5bjdSCACc8MirejccnalK 12 | OPMNtceZRK0OFiWmh+Hwlyt2OsHQdgiKX8gL9PbKoUeOqrAr/CMjkDhjWhsWcGCC 13 | lrRE5AIpVvScWrewMU8syZc++89mICuDVKalKk/9SYqUITWSturb7hJVJqizevbt 14 | I8wAKO6Ae24RbUHw7fyZJJjG90PQmLwlQR4xuJJuf51x8Gb3aurBQ9P2D1oDDlWB 15 | og2V6fHV67KCpF2rdfOYu9B8ghja9XMYSkF7mRU6wb5jBeLniLot+eOX+UCgn6Dl 16 | MivyVx1OtnRRnphIp/sF08IDQ4GFfo+HSugx1goylm7XUalryL1p+2dkaqTMCWty 17 | g08lgv83iQFRBBMBCgA7FiEEq9AJEwGdY1S6HZoTKDn+DXlhmLEFAlyh+BQDBQF4 18 | GYY8W14+XStbQC5dZ2VudG9vXC5vcmc+JAAACgkQKDn+DXlhmLEeBAgAgd1I5RVg 19 | 7Sq1IKNHUFVZDUmi9lkQpERWOJ1HXwbkPHWgtxwmH3O2QaCS8vcUGpUZa1iYe5wP 20 | 5OMlJ0xrJeezFxT1OQgkm+ABYyEShI9Sqz267Qo54o3uDnqvF3pMfl2mehHITp4i 21 | 1wr0Dw4vTF/yZA3MW5/w+7VtQ/ufKkmLeAQPBxGU3+o1RPzBjLEbACPpu7J+Rc5H 22 | iug0b2RP7Q3O3p2cqJ3h7UcK4V0aNJE7k58BlXfmcNEfhY3hanMOzEnNnP0+YQhe 23 | 0UxbILq+QzHffPqamOoect1lhP6SrYoEsQBnAjIxUrf/II6d9Ngu0bLJfjBVmNOy 24 | O6sz1jq8f8alRQ== 25 | =9ay7 26 | -----END PGP PUBLIC KEY BLOCK----- 27 | -------------------------------------------------------------------------------- /directory_bootstrap/resources/gentoo/2C13823B8237310FA213034930D132FF0FF50EEB.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | mQENBFyh9HEBCADs8HgquPQptiGir0O4t/3E373wVLuH9KG7ALECShsS7i2/iH0k 4 | YI/8aQ0noymIR3bN14bKdNdm20oRH7PBaR/Fhl8wfj3khSQWBoyFwfpVK789WuWP 5 | 2SYBo9FpUe/YES1Da0CX8Ti3FgA/RVBlKqkcqL2LH32SRjehuiUm82HR+ulpU+ox 6 | uuFLj0ycJIMOF/TSiFNupjUl8XwwiyTlr0a5GNfAc7CLBxJHxVMSyNggYN8SMmId 7 | ECHXHGWN6SOcA+Zpd+I8FX574zS19T7CNe51cGfOdfhOWNK18NJ66imF4z374vfM 8 | oI1B70CYiv8slVEBrZ3BJE2oU9JuCTpnD/h3ABEBAAG0R0dlbnRvbyBBdXRob3Jp 9 | dHkgS2V5IEwyIGZvciBEZXZlbG9wZXJzIDxvcGVucGdwLWF1dGgrbDItZGV2QGdl 10 | bnRvby5vcmc+iQFMBBMBCgA2AhsBBQsJCg0EAxUKCAIeAQIXgBYhBCwTgjuCNzEP 11 | ohMDSTDRMv8P9Q7rBQJcxGw8BQkCWmowAAoJEDDRMv8P9Q7rzlkH/iXaQ3f0C15C 12 | bWg7SNYmZ+DaSL12j2CWIqmn/KeVQn5Fiza1bZl6jOrfyaxbnOYnIrjlLICkKkhN 13 | KpYhoKh3qxbpePRysRLA+S6YV0lyp8RibIDZvUZvkEnWmtK2a9AQbTMKuuFlNjWU 14 | 5cs4W0AexvkCIeTF5d+1lW2Hc0/26Uyq0ctkDfPBfKblXcVPxbZ/uKldxHwvvMYD 15 | DqoXwyoUkfpUpT7U2256h17U1QaDcy930YLW9G/+RSk+vJN2sM3Hhgvyfj88IfFj 16 | dzRDSDHMIZcrAN+yLe1pOv8QFul7gO76Glt449kXcp4s/3RPKWQgc1eW1OShXAT+ 17 | 3zbNmvP6Bo+JAUwEEwEKADYWIQQsE4I7gjcxD6ITA0kw0TL/D/UO6wUCXKH0cQIb 18 | AQUJAWqHUQULCQoNBAMVCggCHgECF4AACgkQMNEy/w/1Duv3pwgAxug8gJAi+6sb 19 | 1o6pHzafjZ/dnBZxerqiAbAQ82AlS5ZfKRAojkYBC3eA3TyErdGeR5IVihJ6Qm4y 20 | ypZ1fLAZFzIRGvdM7nuea9tyGGEJxAkLpKY9Sv4c/aoIfr8q4beWLhqFH7veFFxH 21 | kwFQe634NlYYxS6GUCt/56AOlz5eG+QENgpOAD856VgVqnGy4SPEh4YcZhZh+anA 22 | TOv9tJSeBFaAd8DhIqskvkbEtUU3OaZrj9Ke+rLyLMH3QHpgJ2vwr0l26yUPzwRw 23 | A5RwhipCEIWND3TTfESFTY6hwnMA/HO7Qex9KBu2Y7jGb97TBUGbvYFPidOkPq4Q 24 | HqdrJy1UoYkBUQQTAQoAOxYhBKvQCRMBnWNUuh2aEyg5/g15YZixBQJcofhPAwUB 25 | eBmGPFtePl0rW0AuXWdlbnRvb1wub3JnPiQAAAoJECg5/g15YZixTCIH/iIeWcFT 26 | V8k0tyvsX8bIa0hnrnccLMJHHrD01Zwz6irBGTzufWZ4e05Ul6Il5qaZW/BDavG2 27 | QELlM7uoq2W0mTqPbkdIj0+0GXpSgL8Wx3CfUzq9t+pkkzm2Ira3q9JMU0s1f2q8 28 | VF6yw/WGY062kWI45xE53H8RoRXStZLmwW0yuBHYTrSsJr0c0DuALPeCbJPcuST9 29 | Ns9Kw13ykmuyaBY/zdttx7rSDP2EJxjYMLYdZYzrMqdKCmnDqbv9HWmi01s6TC94 30 | 4dA+R6LiZKSUSiH10E7PJt/ffaP32WOZtZpgboap8iTbz5TxtLNTOmgUn++MHvIC 31 | vaPAur3PLKc6EEo= 32 | =QV3L 33 | -----END PGP PUBLIC KEY BLOCK----- 34 | -------------------------------------------------------------------------------- /directory_bootstrap/resources/gentoo/ABD00913019D6354BA1D9A132839FE0D796198B1.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 64 | sSUa/7U= 65 | =mfr3 66 | -----END PGP PUBLIC KEY BLOCK----- 67 | -------------------------------------------------------------------------------- /directory_bootstrap/resources/gentoo/EF9538C9E8E64311A52CDEDFA13D0EF1914E7A72.asc: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PUBLIC KEY BLOCK----- 2 | 3 | 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 133 | OpFbZKiOaw== 134 | =0+nW 135 | -----END PGP PUBLIC KEY BLOCK----- 136 | -------------------------------------------------------------------------------- /directory_bootstrap/resources/gentoo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/resources/gentoo/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/setup-pypi-readme.rst: -------------------------------------------------------------------------------- 1 | ``directory-bootstrap`` 2 | ======================= 3 | 4 | Command line tool for creating chroots. 5 | 6 | Technically a meta-package to pull in 7 | package `image-bootstrap `_, 8 | that contains the actual ``directory-bootstrap`` code, 9 | alongside its big brother ``image-bootstrap`` 10 | that creates bootable virtual machine images. 11 | 12 | For more details on either of these please check out 13 | https://github.com/hartwork/image-bootstrap . 14 | 15 | Enjoy! 16 | -------------------------------------------------------------------------------- /directory_bootstrap/setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # Copyright (C) 2018 Sebastian Pipping 3 | # Licensed under AGPL v3 or later 4 | 5 | from textwrap import dedent 6 | 7 | from setuptools import setup 8 | 9 | 10 | if __name__ == '__main__': 11 | setup( 12 | name='directory-bootstrap', 13 | description='Command line tool for creating chroots', 14 | long_description=open('setup-pypi-readme.rst', 'r').read(), 15 | license='AGPL v3 or later', 16 | version='1', 17 | author='Sebastian Pipping', 18 | author_email='sebastian@pipping.org', 19 | url='https://github.com/hartwork/image-bootstrap', 20 | install_requires=[ 21 | 'image-bootstrap', 22 | ], 23 | classifiers=[ 24 | 'Development Status :: 4 - Beta', 25 | 'Environment :: Console', 26 | 'Intended Audience :: Developers', 27 | 'Intended Audience :: End Users/Desktop', 28 | 'Intended Audience :: System Administrators', 29 | 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', 30 | 'Natural Language :: English', 31 | 'Operating System :: POSIX :: Linux', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Topic :: System :: Installation/Setup', 34 | 'Topic :: Utilities', 35 | ], 36 | ) 37 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/shared/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/shared/byte_size.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | _UNIT_LABELS = ( 7 | 'byte', 8 | 'KiB', 9 | 'MiB', 10 | 'GiB', 11 | 'TiB', 12 | ) 13 | 14 | 15 | def format_byte_size(size_bytes): 16 | FACTOR = 1024 17 | for exponent, unit in enumerate(_UNIT_LABELS): 18 | if size_bytes < FACTOR: 19 | if size_bytes < FACTOR / 2: 20 | final_unit = unit 21 | else: 22 | final_unit = _UNIT_LABELS[exponent + 1] 23 | size_bytes /= float(FACTOR) 24 | 25 | value = str('%.3f' % size_bytes).rstrip('0').rstrip('.') 26 | return '%s %s' % (value, final_unit) 27 | 28 | size_bytes /= float(FACTOR) 29 | else: 30 | raise ValueError('Byte size too large to be supported') 31 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/commands.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import errno 5 | import os 6 | import subprocess 7 | import time 8 | 9 | COMMAND_BLKID = 'blkid' 10 | COMMAND_BLOCKDEV = 'blockdev' 11 | COMMAND_CHMOD = 'chmod' 12 | COMMAND_CHROOT = 'chroot' 13 | COMMAND_CP = 'cp' 14 | COMMAND_DB_DUMP = 'db_dump' 15 | COMMAND_EXTLINUX = 'extlinux' 16 | COMMAND_FILE = 'file' 17 | COMMAND_FIND = 'find' 18 | COMMAND_GPG = 'gpg' 19 | COMMAND_INSTALL_MBR = 'install-mbr' 20 | COMMAND_KPARTX = 'kpartx' 21 | COMMAND_LSB_RELEASE = 'lsb_release' 22 | COMMAND_MD5SUM = 'md5sum' 23 | COMMAND_MKDIR = 'mkdir' 24 | COMMAND_MKFS_EXT4 = 'mkfs.ext4' 25 | COMMAND_MOUNT = 'mount' 26 | COMMAND_PARTED = 'parted' 27 | COMMAND_PARTPROBE = 'partprobe' 28 | COMMAND_RM = 'rm' 29 | COMMAND_RMDIR = 'rmdir' 30 | COMMAND_RPM = 'rpm' 31 | COMMAND_SED = 'sed' 32 | COMMAND_SHA512SUM = 'sha512sum' 33 | COMMAND_TAR = 'tar' 34 | COMMAND_TUNE2FS = 'tune2fs' 35 | COMMAND_UMOUNT = 'umount' 36 | COMMAND_UNAME = 'uname' 37 | COMMAND_UNSHARE = 'unshare' 38 | COMMAND_UNXZ = 'unxz' 39 | COMMAND_WGET = 'wget' 40 | COMMAND_YUM = 'yum' 41 | 42 | 43 | EXIT_COMMAND_NOT_FOUND = 127 44 | 45 | 46 | def check_call__keep_trying(executor, cmd): 47 | for i in range(3): 48 | try: 49 | executor.check_call(cmd) 50 | except subprocess.CalledProcessError as e: 51 | if e.returncode == EXIT_COMMAND_NOT_FOUND: 52 | raise 53 | time.sleep(1) 54 | else: 55 | break 56 | 57 | 58 | def find_command(command): 59 | assert not command.startswith('/') 60 | 61 | dirs = os.environ['PATH'].split(':') 62 | for _dir in dirs: 63 | abs_path = os.path.join(_dir, command) 64 | if os.path.exists(abs_path): 65 | return abs_path 66 | 67 | raise OSError(EXIT_COMMAND_NOT_FOUND, 'Command "%s" not found in PATH.' \ 68 | % command) 69 | 70 | 71 | def check_for_commands(messenger, commands_to_check_for): 72 | infos_produced = False 73 | 74 | missing_files = [] 75 | missing_commands = [] 76 | for command in sorted(set(c for c in commands_to_check_for if c is not None)): 77 | if command.startswith('/'): 78 | abs_path = command 79 | if not os.path.exists(abs_path): 80 | missing_files.append(abs_path) 81 | continue 82 | 83 | try: 84 | abs_path = find_command(command) 85 | except OSError as e: 86 | if e.errno != EXIT_COMMAND_NOT_FOUND: 87 | raise 88 | missing_commands.append(command) 89 | messenger.error('Checking for %s... NOT FOUND' % command) 90 | else: 91 | messenger.info('Checking for %s... %s' % (command, abs_path)) 92 | infos_produced = True 93 | 94 | if missing_files: 95 | raise OSError(errno.ENOENT, 'File "%s" not found.' \ 96 | % missing_files[0]) 97 | 98 | if missing_commands: 99 | raise OSError(EXIT_COMMAND_NOT_FOUND, 'Command "%s" not found in PATH.' \ 100 | % missing_commands[0]) 101 | 102 | if infos_produced: 103 | messenger.info_gap() 104 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/executor.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import os 7 | import subprocess 8 | import sys 9 | 10 | 11 | _WANTED_PATHS = ( 12 | '/usr/local/sbin', 13 | '/usr/local/bin', 14 | '/usr/sbin', 15 | '/usr/bin', 16 | '/sbin', 17 | '/bin', 18 | ) 19 | 20 | 21 | def _insert_before_after(list_, befores, element, afters, strict=False): 22 | """ 23 | Insert somewhere after certain elements but also before certain others. 24 | 25 | >>> list_ = [2, 0, 0, 1, 0, 0, 5, 6, 0] 26 | >>> _insert_before_after(list_, [1, 2], 3, [5, 6]) 27 | >>> list_ 28 | [2, 0, 0, 1, 3, 0, 0, 5, 6, 0] 29 | """ 30 | def or_default(func, arg, default): 31 | try: 32 | return func(arg) 33 | except ValueError: 34 | return default 35 | 36 | max_before_index = or_default( 37 | max, (or_default(list_.index, e, -1) for e in befores), 38 | -1) 39 | 40 | min_afters_index = or_default( 41 | min, (or_default(list_.index, e, len(list_)) for e in afters), 42 | len(list_)) 43 | 44 | if max_before_index >= min_afters_index: 45 | if strict: 46 | raise Exception('Cannot satisfy "befores" and "after"' 47 | ' at the same time' 48 | ' with this particular list') 49 | else: 50 | insertion_index = len(list_) 51 | else: 52 | insertion_index = max_before_index + 1 53 | 54 | list_.insert(insertion_index, element) 55 | 56 | 57 | def _sanitize_path(path): 58 | """ 59 | Arch has a rather short $PATH: 60 | ``` 61 | # env -i bash -c 'sed "s,:,\n,g" <<<"$PATH"' 62 | /usr/local/sbin 63 | /usr/local/bin 64 | /usr/bin 65 | ``` 66 | 67 | With their symlinks it makes sense: 68 | ``` 69 | # ls -l /bin /sbin /usr/sbin 70 | lrwxrwxrwx 1 root root 7 Oct 17 07:32 /bin -> usr/bin 71 | lrwxrwxrwx 1 root root 7 Oct 17 07:32 /sbin -> usr/bin 72 | lrwxrwxrwx 1 root root 3 Oct 17 07:32 /usr/sbin -> bin 73 | ``` 74 | 75 | Now if we call chroot on Arch, that short $PATH is used 76 | in a distro made for /usr/sbin to be in $PATH. Hence 77 | we put it in ourselves. 78 | 79 | https://github.com/hartwork/image-bootstrap/issues/62 80 | """ 81 | 82 | future_paths = path.split(os.pathsep) 83 | 84 | tasks = [(_WANTED_PATHS[:i], wanted_path, _WANTED_PATHS[i + 1:]) 85 | for i, wanted_path in enumerate(_WANTED_PATHS)] 86 | 87 | for befores, element, afters in tasks: 88 | if element in future_paths: 89 | continue 90 | 91 | _insert_before_after(future_paths, befores, element, afters) 92 | 93 | return os.pathsep.join(future_paths) 94 | 95 | 96 | def sanitize_path(env=None): 97 | if env is None: 98 | env = os.environ 99 | 100 | env['PATH'] = _sanitize_path(env['PATH']) 101 | 102 | 103 | class Executor(object): 104 | def __init__(self, messenger, stdout=None, stderr=None): 105 | self._messenger = messenger 106 | self._announce_target = stdout or sys.stdout 107 | self._default_stdout = stdout or sys.stdout 108 | self._default_stderr = stderr or sys.stderr 109 | 110 | def check_call(self, argv, env=None, cwd=None): 111 | self._messenger.announce_command(argv) 112 | subprocess.check_call(argv, 113 | stdout=self._default_stdout, 114 | stderr=self._default_stderr, 115 | env=self._without_pythonpath(env), 116 | cwd=cwd, 117 | ) 118 | 119 | def check_output(self, argv): 120 | self._messenger.announce_command(argv) 121 | return subprocess.check_output(argv, stderr=self._default_stderr) 122 | 123 | def _without_pythonpath(self, env): 124 | if env is None: 125 | env = os.environ 126 | 127 | return {k: v for k, v in env.items() if k != 'PYTHONPATH'} 128 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/loaders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/shared/loaders/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/shared/loaders/_argparse.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import sys 7 | 8 | try: 9 | from argparse import ArgumentParser, \ 10 | RawDescriptionHelpFormatter 11 | except ImportError: 12 | print('ERROR: Please use Python >=2.7 or install argparse ' 13 | '(https://pypi.python.org/pypi/argparse). ' 14 | 'Thank you!', file=sys.stderr) 15 | sys.exit(1) 16 | 17 | # Mark as used 18 | ArgumentParser 19 | RawDescriptionHelpFormatter 20 | 21 | del sys 22 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/loaders/_bs4.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import sys 7 | 8 | try: 9 | from bs4 import BeautifulSoup 10 | except ImportError: 11 | print('ERROR: Please install Beautiful Soup ' 12 | '(https://pypi.python.org/pypi/beautifulsoup4). ' 13 | 'Thank you!', file=sys.stderr) 14 | sys.exit(1) 15 | 16 | # Mark as used 17 | BeautifulSoup 18 | 19 | del sys 20 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/loaders/_colorama.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import sys 7 | 8 | try: 9 | from colorama import Fore, Style 10 | except ImportError: 11 | print('ERROR: Please install Colorama ' 12 | '(https://pypi.python.org/pypi/colorama). ' 13 | 'Thank you!', file=sys.stderr) 14 | sys.exit(1) 15 | 16 | # Mark as used 17 | Fore 18 | Style 19 | 20 | del sys 21 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/loaders/_requests.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import sys 7 | 8 | try: 9 | from requests import get 10 | from requests.exceptions import HTTPError 11 | except ImportError as e: 12 | print('ERROR: Please install Requests ' 13 | '(https://pypi.python.org/pypi/requests). ' 14 | 'Thank you!', file=sys.stderr) 15 | sys.exit(1) 16 | 17 | # Create pseudeo-module forwarder 18 | class _ExceptionsModule: 19 | pass 20 | exceptions = _ExceptionsModule() 21 | exceptions.HTTPError = HTTPError 22 | 23 | # Mark as used 24 | get 25 | 26 | del sys 27 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/messenger.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import re 7 | import sys 8 | 9 | from directory_bootstrap.shared.loaders._colorama import Fore, Style 10 | from directory_bootstrap.shared.metadata import ( 11 | GITHUB_HOME_URL, RELEASE_DATE_STR, VERSION_STR) 12 | 13 | _NEEDS_ESCAPING = re.compile('([!`"\'$ \\\\{}()?*&<>;])') 14 | 15 | VERBOSITY_QUIET = object() 16 | VERBOSITY_VERBOSE = object() 17 | 18 | BANNER = r""" 19 | _ __ __ __ 20 | (_)_ _ ___ ____ ____ ___ / / ___ ___ / /____ / /________ ____ 21 | / / ' \/ _ `/ _ `/ -_)/__// _ \/ _ \/ _ \/ __(_-. 27 | Please report bugs at %(github_home)s. Thank you! 28 | """ % { 29 | '3456789_123456789_': '%*s' \ 30 | % (len('%(3456789_123456789_)s'), 31 | 'v%s :: %s' % (VERSION_STR, RELEASE_DATE_STR)), 32 | 'github_home': GITHUB_HOME_URL, 33 | } 34 | 35 | 36 | def fix_output_encoding(): 37 | """ 38 | Fixes program invocation a la "...... |& tee file.log" 39 | to not end up with UnicodeEncodeError 40 | """ 41 | if sys.stdout.encoding is None: 42 | import codecs 43 | sys.stdout = codecs.getwriter('utf-8')(sys.stdout) 44 | if sys.stderr.encoding is None: 45 | import codecs 46 | sys.stderr = codecs.getwriter('utf-8')(sys.stderr) 47 | 48 | 49 | class Messenger(object): 50 | def __init__(self, verbosity, colorize): 51 | self._infos_wanted = verbosity is not VERBOSITY_QUIET 52 | self._warnings_wanted = verbosity is not VERBOSITY_QUIET 53 | self._commands_wanted = verbosity is VERBOSITY_VERBOSE 54 | self._colorize = colorize 55 | 56 | def colorize(self, text, fore=None, style=None): 57 | if not self._colorize: 58 | return text 59 | 60 | chunks = [] 61 | if fore: 62 | chunks.append(fore) 63 | if style: 64 | chunks.append(style) 65 | chunks.append(text) 66 | if fore or style: 67 | chunks.append(Style.RESET_ALL) 68 | return ''.join(chunks) 69 | 70 | def banner(self): 71 | if not self._infos_wanted: 72 | return 73 | 74 | print(BANNER) 75 | print() 76 | 77 | def escape_shell(self, text): 78 | escaped = _NEEDS_ESCAPING.sub('\\\\\\1', text) 79 | if not escaped: 80 | return "''" 81 | return escaped 82 | 83 | def announce_command(self, argv): 84 | if not self._commands_wanted: 85 | return 86 | text = '# %s' % ' '.join((self.escape_shell(e) for e in argv)) 87 | 88 | sys.stderr.flush() 89 | print(self.colorize(text, Fore.CYAN)) 90 | sys.stdout.flush() 91 | 92 | def info(self, text): 93 | if not self._infos_wanted: 94 | return 95 | print(self.colorize(text, Fore.GREEN)) 96 | 97 | def warn(self, text): 98 | if not self._warnings_wanted: 99 | return 100 | print(self.colorize('Warning: ' + text, Fore.MAGENTA, Style.BRIGHT)) 101 | 102 | def error(self, text): 103 | print(self.colorize('Error: ' + text, Fore.RED, Style.BRIGHT), file=sys.stderr) 104 | 105 | def info_gap(self): 106 | if not self._infos_wanted: 107 | return 108 | print() 109 | 110 | def encourage_bug_reports(self): 111 | print('If this looks like a bug to you, please file a report at %s. Thank you!' \ 112 | % GITHUB_HOME_URL, file=sys.stderr) 113 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/metadata.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | PACKAGE_NAME = 'image-bootstrap' 5 | 6 | GITHUB_HOME_URL = 'https://github.com/hartwork/image-bootstrap' 7 | 8 | DESCRIPTION = 'Command line tool for creating bootable virtual machine images' 9 | 10 | _VERSION = (2, 0, 5) 11 | VERSION_STR = '.'.join((str(e) for e in _VERSION)) 12 | 13 | _RELEASE_DATE = (2021, 1, 8) 14 | RELEASE_DATE_STR = '-'.join(('%02d' % e for e in _RELEASE_DATE)) 15 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/mount.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | from directory_bootstrap.shared.commands import ( 5 | COMMAND_UMOUNT, check_call__keep_trying) 6 | 7 | 8 | def try_unmounting(executor, abs_path): 9 | cmd = [ 10 | COMMAND_UMOUNT, 11 | abs_path, 12 | ] 13 | check_call__keep_trying(executor, cmd) 14 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/namespace.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import errno 5 | import os 6 | from ctypes import CDLL, c_char_p, c_int, cast, get_errno 7 | 8 | _CLONE_NEWNS = 0x00020000 9 | _CLONE_NEWUTS = 0x04000000 10 | 11 | _lib_c = CDLL("libc.so.6", use_errno=True) 12 | 13 | 14 | def unshare_current_process(messenger): 15 | messenger.info('Unsharing Linux namespaces (mount, UTS/hostname)...') 16 | ret = _lib_c.unshare(c_int(_CLONE_NEWNS | _CLONE_NEWUTS)) 17 | if ret: 18 | _errno = get_errno() or errno.EPERM 19 | raise OSError(_errno, 'Unsharing Linux namespaces failed: ' + os.strerror(_errno)) 20 | 21 | 22 | def set_hostname(hostname): 23 | hostname_char_p = cast(hostname.encode('utf-8'), c_char_p) 24 | hostname_len_size_t = _lib_c.strlen(hostname_char_p) 25 | ret = _lib_c.sethostname(hostname_char_p, hostname_len_size_t) 26 | if ret: 27 | _errno = get_errno() or errno.EPERM 28 | raise OSError(_errno, 'Setting hostname failed: ' + os.strerror(_errno)) 29 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/output_control.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import os 5 | import subprocess 6 | import sys 7 | import traceback 8 | 9 | from directory_bootstrap.shared.messenger import ( 10 | VERBOSITY_QUIET, VERBOSITY_VERBOSE) 11 | 12 | _COLORIZE_NEVER = 'never' 13 | _COLORIZE_ALWAYS = 'always' 14 | _COLORIZE_AUTO = 'auto' 15 | 16 | 17 | def add_output_control_options(parser): 18 | output = parser.add_argument_group('text output configuration') 19 | output.add_argument('--color', default=_COLORIZE_AUTO, choices=[_COLORIZE_NEVER, _COLORIZE_ALWAYS, _COLORIZE_AUTO], 20 | help='toggle output color (default: %(default)s)') 21 | output.add_argument('--debug', action='store_true', 22 | help='enable debugging') 23 | output.add_argument('--quiet', dest='verbosity', action='store_const', const=VERBOSITY_QUIET, 24 | help='limit output to error messages') 25 | output.add_argument('--verbose', dest='verbosity', action='store_const', const=VERBOSITY_VERBOSE, 26 | help='increase verbosity') 27 | 28 | 29 | def is_color_wanted(options): 30 | if options.color == _COLORIZE_AUTO: 31 | colorize = os.isatty(sys.stdout.fileno()) 32 | else: 33 | colorize = options.color == _COLORIZE_ALWAYS 34 | 35 | return colorize 36 | 37 | 38 | def run_handle_errors(main_function, messenger, options): 39 | try: 40 | main_function(messenger, options) 41 | except KeyboardInterrupt: 42 | messenger.info('Interrupted.') 43 | raise 44 | except BaseException as e: 45 | if options.debug: 46 | traceback.print_exc(file=sys.stderr) 47 | 48 | if isinstance(e, subprocess.CalledProcessError): 49 | # Manual work to avoid list square brackets in output 50 | command_flat = ' '.join((messenger.escape_shell(e) for e in e.cmd)) 51 | text = 'Command "%s" returned non-zero exit status %s' % (command_flat, e.returncode) 52 | elif hasattr(e, '_ib_abs_script_filename'): 53 | text = '%s (script "%s")' % (str(e), e._ib_abs_script_filename) 54 | else: 55 | text = str(e) 56 | 57 | messenger.error(text) 58 | messenger.encourage_bug_reports() 59 | sys.exit(1) 60 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/resolv_conf.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | 7 | def filter_copy_resolv_conf(messenger, abs_etc_resolv_conf, output_filename): 8 | messenger.info('Writing file "%s" (based on file "%s")...' 9 | % (output_filename, abs_etc_resolv_conf)) 10 | 11 | with open(abs_etc_resolv_conf) as input_f: 12 | with open(output_filename, 'w') as output_f: 13 | for l in input_f: 14 | line = l.rstrip() 15 | if line.startswith('nameserver'): 16 | print(line, file=output_f) 17 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/shared/test/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/shared/test/test_byte_size.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | from unittest import TestCase 7 | 8 | from directory_bootstrap.shared.byte_size import format_byte_size 9 | 10 | 11 | class TestByteSizeFormatter(TestCase): 12 | def test_some(self): 13 | for size_bytes, expected in ( 14 | (0, '0 byte'), 15 | (1, '1 byte'), 16 | (2, '2 byte'), 17 | (511, '511 byte'), 18 | (512, '0.5 KiB'), 19 | (513, '0.501 KiB'), 20 | (1023, '0.999 KiB'), 21 | (1024, '1 KiB'), 22 | (1025, '1.001 KiB'), 23 | (1024 * 1024 - 1, '1 MiB'), 24 | (1024 * 1024, '1 MiB'), 25 | (1024 * 1024 + 1, '1 MiB'), 26 | (1024**3, '1 GiB'), 27 | (1024**4, '1 TiB'), 28 | ): 29 | received = format_byte_size(size_bytes) 30 | self.assertEqual(received, expected) 31 | -------------------------------------------------------------------------------- /directory_bootstrap/shared/test/test_path_extension.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from ..executor import _sanitize_path 4 | 5 | 6 | class PathExtensionTest(unittest.TestCase): 7 | 8 | def test_path_extension__arch_root(self): 9 | original_path = '/usr/local/sbin:/usr/local/bin:/usr/bin' 10 | expected_path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' 11 | 12 | self.assertEqual(_sanitize_path(original_path), expected_path) 13 | 14 | def test_path_extension__debian_jessie_stretch_root(self): 15 | original_path = '/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/bin:/sbin:.' 16 | expected_path = original_path 17 | 18 | self.assertEqual(_sanitize_path(original_path), expected_path) 19 | 20 | def test_path_extension__stretch_unprivileged(self): 21 | original_path = '/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games' 22 | expected_path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/games:/usr/games' 23 | 24 | self.assertEqual(_sanitize_path(original_path), expected_path) 25 | 26 | def test_path_extension__disjoint(self): 27 | original_path = '/one:/two' 28 | expected_path = '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/one:/two' 29 | 30 | self.assertEqual(_sanitize_path(original_path), expected_path) 31 | -------------------------------------------------------------------------------- /directory_bootstrap/tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/tools/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/tools/stage3_latest_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import re 7 | 8 | _year = '([2-9][0-9]{3})' 9 | _month = '(0[1-9]|1[0-2])' 10 | _day = '(0[1-9]|[12][0-9]|3[01])' 11 | _time = '(T[0-9]{6}Z)?' 12 | 13 | _STAGE3_TARBALL_DATE_PATTERN = '^(?P%s%s%s%s)/stage3-(?P[^ -]+)(?P-openrc)?-[0-9]+(T[0-9]+Z)?\\.tar\\.[^ ]+ [1-9]+[0-9]*$' % (_year, _month, _day, _time) 14 | _stage3_tarball_date_matcher = re.compile(_STAGE3_TARBALL_DATE_PATTERN) 15 | 16 | 17 | def find_latest_stage3_date(stage3_latest_file_content, stage3_latest_file_url, architecture): 18 | matches = [] 19 | for line in stage3_latest_file_content.split('\n'): 20 | m = _stage3_tarball_date_matcher.match(line) 21 | if m is None: 22 | continue 23 | if m.group('arch') != architecture: 24 | continue 25 | matches.append(m) 26 | 27 | message = ('Content from %s does not seem to contain ' 28 | 'well-formed OpenRC/default ' 29 | 'stage3 tarball entr(y|ies)' 30 | % stage3_latest_file_url 31 | ) 32 | 33 | if not matches: 34 | raise ValueError(message) 35 | 36 | m = sorted(matches, key=lambda e: e.group(1))[-1] # i.e. most recent 37 | return (int(m.group(2)), int(m.group(3)), int(m.group(4))), m.group(5), m.group('flavor') or '' 38 | -------------------------------------------------------------------------------- /directory_bootstrap/tools/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/directory_bootstrap/tools/test/__init__.py -------------------------------------------------------------------------------- /directory_bootstrap/tools/test/test_stage3_latest_parser.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | from textwrap import dedent 7 | from unittest import TestCase 8 | 9 | from directory_bootstrap.tools.stage3_latest_parser import \ 10 | find_latest_stage3_date 11 | 12 | 13 | class TestStag3LatestParser(TestCase): 14 | def test_(self): 15 | content = dedent("""\ 16 | # Latest as of Mon, 05 Oct 2015 18:30:01 +0000 17 | # ts=1444069801 18 | 20151001/stage3-amd64-20151001.tar.bz2 224211865 19 | 20151001/hardened/stage3-amd64-hardened-20151001.tar.bz2 220165244 20 | 20151001/hardened/stage3-amd64-hardened+nomultilib-20151001.tar.bz2 211952954 21 | 20151001/stage3-amd64-nomultilib-20151001.tar.bz2 214753131 22 | 20150905/uclibc/stage3-amd64-uclibc-hardened-20150905.tar.bz2 138274772 23 | 20150905/uclibc/stage3-amd64-uclibc-vanilla-20150905.tar.bz2 135760218 24 | 20150819/stage3-x32-20150819.tar.bz2 241353307 25 | """) 26 | (year, month, day), _1, arch_flavor = find_latest_stage3_date(content, 'http://distfiles.gentoo.org/releases/amd64/autobuilds/latest-stage3.txt', 'amd64') 27 | self.assertEqual((year, month, day, arch_flavor), (2015, 10, 1, '')) 28 | -------------------------------------------------------------------------------- /image_bootstrap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/image_bootstrap/__init__.py -------------------------------------------------------------------------------- /image_bootstrap/__main__.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import os 5 | import signal 6 | import sys 7 | 8 | from directory_bootstrap.distros.base import \ 9 | add_general_directory_bootstrapping_options 10 | from directory_bootstrap.shared.executor import Executor, sanitize_path 11 | from directory_bootstrap.shared.loaders._argparse import ( 12 | ArgumentParser, RawDescriptionHelpFormatter) 13 | from directory_bootstrap.shared.messenger import ( 14 | BANNER, VERBOSITY_VERBOSE, Messenger, fix_output_encoding) 15 | from directory_bootstrap.shared.metadata import DESCRIPTION, VERSION_STR 16 | from directory_bootstrap.shared.output_control import ( 17 | add_output_control_options, is_color_wanted, run_handle_errors) 18 | from image_bootstrap.distros.arch import ArchStrategy 19 | from image_bootstrap.distros.base import DISTRO_CLASS_FIELD 20 | from image_bootstrap.distros.debian import DebianStrategy 21 | from image_bootstrap.distros.gentoo import GentooStrategy 22 | from image_bootstrap.distros.ubuntu import UbuntuStrategy 23 | from image_bootstrap.engine import ( 24 | BOOTLOADER__AUTO, BOOTLOADER__CHROOT_GRUB2__DEVICE, 25 | BOOTLOADER__CHROOT_GRUB2__DRIVE, BOOTLOADER__HOST_EXTLINUX, 26 | BOOTLOADER__HOST_GRUB2__DEVICE, BOOTLOADER__HOST_GRUB2__DRIVE, 27 | BOOTLOADER__NONE, BootstrapEngine, MachineConfig) 28 | from image_bootstrap.types.disk_id import disk_id_type 29 | from image_bootstrap.types.machine_id import machine_id_type 30 | from image_bootstrap.types.uuid import uuid_type 31 | 32 | _BOOTLOADER_APPROACHES = ( 33 | BOOTLOADER__AUTO, 34 | BOOTLOADER__CHROOT_GRUB2__DEVICE, 35 | BOOTLOADER__CHROOT_GRUB2__DRIVE, 36 | BOOTLOADER__HOST_EXTLINUX, 37 | BOOTLOADER__HOST_GRUB2__DEVICE, 38 | BOOTLOADER__HOST_GRUB2__DRIVE, 39 | BOOTLOADER__NONE 40 | ) 41 | 42 | 43 | def _abspath_or_none(path_or_none): 44 | return path_or_none and os.path.abspath(path_or_none) 45 | 46 | 47 | def _main__level_three(messenger, options): 48 | messenger.banner() 49 | 50 | stdout_wanted = options.verbosity is VERBOSITY_VERBOSE 51 | 52 | if stdout_wanted: 53 | child_process_stdout = None 54 | else: 55 | child_process_stdout = open('/dev/null', 'w') 56 | 57 | sanitize_path() 58 | 59 | executor = Executor(messenger, stdout=child_process_stdout) 60 | 61 | machine_config = MachineConfig( 62 | options.hostname, 63 | options.architecture, 64 | options.root_password, 65 | _abspath_or_none(options.root_password_file), 66 | os.path.abspath(options.resolv_conf), 67 | options.disk_id, 68 | options.first_partition_uuid, 69 | options.machine_id, 70 | options.bootloader_approach, 71 | options.bootloader_force, 72 | options.with_openstack, 73 | ) 74 | 75 | bootstrap = BootstrapEngine( 76 | messenger, 77 | executor, 78 | machine_config, 79 | _abspath_or_none(options.scripts_dir_pre), 80 | _abspath_or_none(options.scripts_dir_chroot), 81 | _abspath_or_none(options.scripts_dir_post), 82 | os.path.abspath(options.target_path), 83 | options.command_grub2_install, 84 | ) 85 | 86 | distro_class = getattr(options, DISTRO_CLASS_FIELD) 87 | bootstrap.set_distro(distro_class.create(messenger, executor, options)) 88 | 89 | bootstrap.check_release() 90 | bootstrap.select_bootloader() 91 | bootstrap.detect_grub2_install() 92 | bootstrap.check_for_commands() 93 | bootstrap.check_architecture() 94 | bootstrap.check_target_block_device() 95 | bootstrap.check_script_permissions() 96 | bootstrap.process_root_password() 97 | bootstrap.run() 98 | 99 | if not stdout_wanted: 100 | child_process_stdout.close() 101 | 102 | messenger.info('Done.') 103 | 104 | 105 | def _main__level_two(): 106 | parser = ArgumentParser( 107 | prog='image-bootstrap', 108 | description=DESCRIPTION, 109 | epilog=BANNER, 110 | formatter_class=RawDescriptionHelpFormatter, 111 | ) 112 | parser.add_argument('--version', action='version', version=VERSION_STR) 113 | 114 | add_output_control_options(parser) 115 | 116 | machine = parser.add_argument_group('machine configuration') 117 | machine.add_argument('--arch', dest='architecture', default='amd64', 118 | help='architecture (e.g. amd64)') 119 | machine.add_argument('--bootloader', dest='bootloader_approach', 120 | default=BOOTLOADER__AUTO, choices=_BOOTLOADER_APPROACHES, 121 | help='approach to take during bootloader installation (default: %(default)s)') 122 | machine.add_argument('--bootloader-force', default=False, action='store_true', 123 | help='apply more force when installing bootloader (default: disabled)') 124 | machine.add_argument('--hostname', default='machine', metavar='NAME', 125 | help='hostname to set (default: "%(default)s")') 126 | machine.add_argument('--openstack', dest='with_openstack', default=False, action='store_true', 127 | help='prepare for use with OpenStack (default: disabled)') 128 | password_options = machine.add_mutually_exclusive_group() 129 | password_options.add_argument('--password', dest='root_password', metavar='PASSWORD', 130 | help='root password to set (default: password log-in disabled)') 131 | password_options.add_argument('--password-file', dest='root_password_file', metavar='FILE', 132 | help='file to read root password from (default: password log-in disabled)') 133 | machine.add_argument('--resolv-conf', metavar='FILE', default='/etc/resolv.conf', 134 | help='file to copy nameserver entries from (default: %(default)s)') 135 | machine.add_argument('--disk-id', dest='disk_id', metavar='ID', type=disk_id_type, 136 | help='specific disk identifier to apply, e.g. 0x12345678') 137 | machine.add_argument('--first-partition-uuid', dest='first_partition_uuid', metavar='UUID', type=uuid_type, 138 | help='specific UUID to apply to first partition, e.g. c1b9d5a2-f162-11cf-9ece-0020afc76f16') 139 | machine.add_argument('--machine-id', dest='machine_id', metavar='ID', type=machine_id_type, 140 | help='specific machine identifier to apply, e.g. c1b9d5a2f16211cf9ece0020afc76f16') 141 | 142 | script_dirs = parser.add_argument_group('script integration') 143 | script_dirs.add_argument('--scripts-pre', dest='scripts_dir_pre', metavar='DIRECTORY', 144 | help='scripts to run prior to chrooting phase, in alphabetical order') 145 | script_dirs.add_argument('--scripts-chroot', dest='scripts_dir_chroot', metavar='DIRECTORY', 146 | help='scripts to run during chrooting phase, in alphabetical order') 147 | script_dirs.add_argument('--scripts-post', dest='scripts_dir_post', metavar='DIRECTORY', 148 | help='scripts to run after chrooting phase, in alphabetical order') 149 | 150 | commands = parser.add_argument_group('command names') 151 | commands.add_argument('--grub2-install', metavar='COMMAND', dest='command_grub2_install', 152 | help='override grub2-install command') 153 | 154 | general = parser.add_argument_group('general configuration') 155 | add_general_directory_bootstrapping_options(general) 156 | 157 | distros = parser.add_subparsers(title='subcommands (choice of distribution)', 158 | description='Run "%(prog)s DISTRIBUTION --help" for details ' 159 | 'on options specific to that distribution.', 160 | metavar='DISTRIBUTION', help='choice of distribution, pick from:') 161 | 162 | 163 | for strategy_clazz in ( 164 | ArchStrategy, 165 | DebianStrategy, 166 | GentooStrategy, 167 | UbuntuStrategy, 168 | ): 169 | strategy_clazz.add_parser_to(distros) 170 | 171 | 172 | parser.add_argument('target_path', metavar='DEVICE', 173 | help='block device to install to') 174 | 175 | options = parser.parse_args() 176 | 177 | messenger = Messenger(options.verbosity, is_color_wanted(options)) 178 | run_handle_errors(_main__level_three, messenger, options) 179 | 180 | 181 | def main(): 182 | try: 183 | fix_output_encoding() 184 | _main__level_two() 185 | except KeyboardInterrupt: 186 | sys.exit(128 + signal.SIGINT) 187 | 188 | 189 | if __name__ == '__main__': 190 | main() 191 | -------------------------------------------------------------------------------- /image_bootstrap/boot_loaders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/image_bootstrap/boot_loaders/__init__.py -------------------------------------------------------------------------------- /image_bootstrap/boot_loaders/grub2.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import errno 7 | import os 8 | 9 | from directory_bootstrap.shared.commands import COMMAND_CHROOT 10 | 11 | BOOTLOADER__CHROOT_GRUB2__DEVICE = 'chroot-grub2-device' 12 | BOOTLOADER__CHROOT_GRUB2__DRIVE = 'chroot-grub2-drive' 13 | BOOTLOADER__HOST_GRUB2__DEVICE = 'host-grub2-device' 14 | BOOTLOADER__HOST_GRUB2__DRIVE = 'host-grub2-drive' 15 | 16 | BOOTLOADER__CHROOT_GRUB2 = ( 17 | BOOTLOADER__CHROOT_GRUB2__DEVICE, 18 | BOOTLOADER__CHROOT_GRUB2__DRIVE, 19 | ) 20 | 21 | _BOOTLOADER__ANY_GRUB2__DRIVE = ( 22 | BOOTLOADER__CHROOT_GRUB2__DRIVE, 23 | BOOTLOADER__HOST_GRUB2__DRIVE, 24 | ) 25 | 26 | 27 | class GrubTwoInstaller(object): 28 | def __init__(self, 29 | messenger, 30 | executor, 31 | abs_target_path, 32 | bootloader_approach, 33 | bootloader_force, 34 | command_host_grub2_install, 35 | command_chroot_grub2_install, 36 | chroot_env, 37 | abs_mountpoint, 38 | ): 39 | self._messenger = messenger 40 | self._executor = executor 41 | 42 | self._abs_target_path = abs_target_path 43 | self._bootloader_approach = bootloader_approach 44 | self._bootloader_force = bootloader_force 45 | 46 | self._command_host_grub2_install = command_host_grub2_install 47 | 48 | self._command_chroot_grub2_install = command_chroot_grub2_install 49 | self._chroot_env = chroot_env 50 | self._abs_mountpoint = abs_mountpoint 51 | 52 | def _create_bootloader_install_message(self, real_abs_target): 53 | hints = [] 54 | if real_abs_target != os.path.normpath(self._abs_target_path): 55 | hints.append('actually "%s"' % real_abs_target) 56 | hints.append('approach "%s"' % self._bootloader_approach) 57 | 58 | return 'Installing bootloader to device "%s" (%s)...' % ( 59 | self._abs_target_path, ', '.join(hints)) 60 | 61 | def run(self): 62 | real_abs_target = os.path.realpath(self._abs_target_path) 63 | message = self._create_bootloader_install_message(real_abs_target) 64 | 65 | use_chroot = self._bootloader_approach in BOOTLOADER__CHROOT_GRUB2 66 | use_device_map = self._bootloader_approach in _BOOTLOADER__ANY_GRUB2__DRIVE 67 | 68 | chroot_boot_grub = os.path.join(self._abs_mountpoint, 'boot', 'grub') 69 | try: 70 | os.makedirs(chroot_boot_grub, 0o755) 71 | except OSError as e: 72 | if e.errno != errno.EEXIST: 73 | raise 74 | 75 | if use_device_map: 76 | # Write device map just for being able to call grub-install 77 | abs_chroot_device_map = os.path.join(chroot_boot_grub, 'device.map') 78 | grub_drive = '(hd9999)' 79 | self._messenger.info('Writing device map to "%s" (mapping "%s" to "%s")...' \ 80 | % (abs_chroot_device_map, grub_drive, real_abs_target)) 81 | f = open(abs_chroot_device_map, 'w') 82 | print('%s\t%s' % (grub_drive, real_abs_target), file=f) 83 | f.close() 84 | 85 | self._messenger.info(message) 86 | 87 | cmd = [] 88 | 89 | if use_chroot: 90 | cmd += [ 91 | COMMAND_CHROOT, 92 | self._abs_mountpoint, 93 | self._command_chroot_grub2_install, 94 | ] 95 | env = self._chroot_env 96 | else: 97 | cmd += [ 98 | self._command_host_grub2_install, 99 | '--boot-directory', 100 | os.path.join(self._abs_mountpoint, 'boot'), 101 | ] 102 | env = None 103 | 104 | cmd.append('--target=i386-pc') # ensure non-EFI 105 | 106 | if self._bootloader_force: 107 | cmd.append('--force') 108 | 109 | if use_device_map: 110 | cmd.append(grub_drive) 111 | else: 112 | cmd.append(self._abs_target_path) 113 | 114 | self._executor.check_call(cmd, env=env) 115 | 116 | if use_device_map: 117 | os.remove(abs_chroot_device_map) 118 | -------------------------------------------------------------------------------- /image_bootstrap/distros/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/image_bootstrap/distros/__init__.py -------------------------------------------------------------------------------- /image_bootstrap/distros/arch.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import os 7 | from textwrap import dedent 8 | 9 | from directory_bootstrap.distros.arch import ( 10 | SUPPORTED_ARCHITECTURES, ArchBootstrapper) 11 | from directory_bootstrap.shared.commands import ( 12 | COMMAND_CHROOT, COMMAND_CP, COMMAND_FIND, COMMAND_RM, COMMAND_SED, 13 | COMMAND_WGET) 14 | from image_bootstrap.distros.base import DISTRO_CLASS_FIELD, DistroStrategy 15 | 16 | 17 | class ArchStrategy(DistroStrategy): 18 | DISTRO_KEY = 'arch' 19 | DISTRO_NAME_SHORT = 'Arch' 20 | DISTRO_NAME_LONG = 'Arch Linux' 21 | 22 | def __init__(self, messenger, executor, 23 | abs_cache_dir, image_date_triple_or_none, mirror_url, 24 | abs_resolv_conf): 25 | super(ArchStrategy, self).__init__( 26 | messenger, 27 | executor, 28 | abs_cache_dir, 29 | abs_resolv_conf, 30 | ) 31 | 32 | self._image_date_triple_or_none = image_date_triple_or_none 33 | self._mirror_url = mirror_url 34 | 35 | def get_commands_to_check_for(self): 36 | return ArchBootstrapper.get_commands_to_check_for() + [ 37 | COMMAND_CHROOT, 38 | COMMAND_CP, 39 | COMMAND_FIND, 40 | COMMAND_RM, 41 | COMMAND_SED, 42 | COMMAND_WGET, 43 | ] 44 | 45 | def check_architecture(self, architecture): 46 | if architecture == 'amd64': 47 | architecture = 'x86_64' 48 | 49 | if architecture not in SUPPORTED_ARCHITECTURES: 50 | raise ValueError('Architecture "%s" not supported' % architecture) 51 | 52 | return architecture 53 | 54 | def configure_hostname(self, hostname): 55 | self.write_etc_hostname(hostname) 56 | 57 | def allow_autostart_of_services(self, allow): 58 | pass # services are not auto-started on Arch 59 | 60 | def run_directory_bootstrap(self, architecture, bootloader_approach): 61 | self._messenger.info('Bootstrapping %s into "%s"...' 62 | % (self.DISTRO_NAME_SHORT, self._abs_mountpoint)) 63 | 64 | bootstrap = ArchBootstrapper( 65 | self._messenger, 66 | self._executor, 67 | self._abs_mountpoint, 68 | self._abs_cache_dir, 69 | architecture, 70 | self._image_date_triple_or_none, 71 | self._mirror_url, 72 | self._abs_resolv_conf, 73 | ) 74 | bootstrap.run() 75 | 76 | def create_network_configuration(self, use_mtu_tristate): 77 | self._messenger.info('Making sure that network interfaces get named eth*...') 78 | os.symlink('/dev/null', os.path.join(self._abs_mountpoint, 'etc/udev/rules.d/80-net-setup-link.rules')) 79 | 80 | network_filename = os.path.join(self._abs_mountpoint, 'etc/systemd/network/eth0-dhcp.network') 81 | self._messenger.info('Writing file "%s"...' % network_filename) 82 | with open(network_filename, 'w') as f: 83 | if use_mtu_tristate is None: 84 | print(dedent("""\ 85 | [Match] 86 | Name=eth0 87 | 88 | [Network] 89 | DHCP=yes 90 | """), file=f) 91 | else: 92 | d = { 93 | 'use_mtu': 'true' if use_mtu_tristate else 'false', 94 | } 95 | print(dedent("""\ 96 | [Match] 97 | Name=eth0 98 | 99 | [Network] 100 | DHCP=yes 101 | 102 | [DHCP] 103 | UseMTU=%(use_mtu)s 104 | """ % d), file=f) 105 | 106 | def _install_packages(self, package_names): 107 | cmd = [ 108 | COMMAND_CHROOT, 109 | self._abs_mountpoint, 110 | 'pacman', 111 | '--noconfirm', 112 | '--sync', 113 | ] + list(package_names) 114 | self._executor.check_call(cmd, env=self.create_chroot_env()) 115 | 116 | def ensure_chroot_has_grub2_installed(self): 117 | self._install_packages(['grub']) 118 | 119 | def get_chroot_command_grub2_install(self): 120 | return 'grub-install' 121 | 122 | def generate_grub_cfg_from_inside_chroot(self): 123 | cmd = [ 124 | COMMAND_CHROOT, 125 | self._abs_mountpoint, 126 | 'grub-mkconfig', 127 | '-o', '/boot/grub/grub.cfg', 128 | ] 129 | self._executor.check_call(cmd, env=self.create_chroot_env()) 130 | 131 | def adjust_initramfs_generator_config(self): 132 | abs_linux_preset = os.path.join(self._abs_mountpoint, 'etc', 'mkinitcpio.d', 'linux.preset') 133 | self._messenger.info('Adjusting "%s"...' % abs_linux_preset) 134 | cmd_sed = [ 135 | COMMAND_SED, 136 | 's,^[# \\t]*default_options=.*,default_options="-S autodetect" # set by image-bootstrap,g', 137 | '-i', abs_linux_preset, 138 | ] 139 | self._executor.check_call(cmd_sed) 140 | 141 | def generate_initramfs_from_inside_chroot(self): 142 | cmd_mkinitcpio = [ 143 | COMMAND_CHROOT, 144 | self._abs_mountpoint, 145 | 'mkinitcpio', 146 | '-p', 'linux', 147 | ] 148 | self._executor.check_call(cmd_mkinitcpio, env=self.create_chroot_env()) 149 | 150 | def _setup_pacman_reanimation(self): 151 | self._messenger.info('Installing haveged (for reanimate-pacman, only)...') 152 | self._install_packages(['haveged']) 153 | 154 | local_reanimate_path = '/usr/sbin/reanimate-pacman' 155 | 156 | full_reanimate_path = os.path.join(self._abs_mountpoint, local_reanimate_path.lstrip('/')) 157 | self._messenger.info('Writing file "%s"...' % full_reanimate_path) 158 | with open(full_reanimate_path, 'w') as f: 159 | print(dedent("""\ 160 | #! /bin/bash 161 | if [[ -e /etc/pacman.d/gnupg ]]; then 162 | exit 0 163 | fi 164 | 165 | haveged -F & 166 | haveged_pid=$! 167 | 168 | /usr/bin/pacman-key --init 169 | /usr/bin/pacman-key --populate archlinux 170 | 171 | kill -9 "${haveged_pid}" 172 | """), file=f) 173 | os.fchmod(f.fileno(), 0o755) 174 | 175 | pacman_reanimation_service = os.path.join(self._abs_mountpoint, 176 | 'etc/systemd/system/pacman-reanimation.service') 177 | self._messenger.info('Writing file "%s"...' % pacman_reanimation_service) 178 | with open(pacman_reanimation_service, 'w') as f: 179 | print(dedent("""\ 180 | [Unit] 181 | Description=Pacman reanimation 182 | 183 | [Service] 184 | ExecStart=/bin/true 185 | ExecStartPost=%s 186 | 187 | [Install] 188 | WantedBy=multi-user.target 189 | """ % local_reanimate_path), file=f) 190 | 191 | self._make_services_autostart(['pacman-reanimation']) 192 | 193 | def perform_in_chroot_shipping_clean_up(self): 194 | self._setup_pacman_reanimation() 195 | 196 | # NOTE: After this, calling pacman needs reanimation, first 197 | pacman_gpg_path = os.path.join(self._abs_mountpoint, 'etc/pacman.d/gnupg') 198 | self._messenger.info('Deleting pacman keys at "%s"...' % pacman_gpg_path) 199 | cmd = [ 200 | COMMAND_RM, 201 | '-Rv', pacman_gpg_path, 202 | ] 203 | self._executor.check_call(cmd) 204 | 205 | 206 | def perform_post_chroot_clean_up(self): 207 | self._messenger.info('Cleaning chroot pacman cache...') 208 | cmd = [ 209 | COMMAND_FIND, 210 | os.path.join(self._abs_mountpoint, 'var/cache/pacman/pkg/'), 211 | '-type', 'f', 212 | '-delete', 213 | ] 214 | self._executor.check_call(cmd) 215 | 216 | def install_dhcp_client(self): 217 | pass # already installed (part of systemd) 218 | 219 | def install_sudo(self): 220 | self._install_packages(['sudo']) 221 | 222 | def install_cloud_init_and_friends(self): 223 | self._install_packages(['cloud-init']) 224 | self.disable_cloud_init_syslog_fix_perms() 225 | self.install_growpart() 226 | 227 | # cloud-unit makes use of command "hostname" 228 | # that is provided by package "inetutils" in Arch but the 229 | # cloud-init packaging lacks a runtime dependency on inetutils, 230 | # see Arch bug https://bugs.archlinux.org/task/67941 . 231 | # Installing inetutils ourselves helps while waiting for a fix in Arch. 232 | # See also: https://github.com/hartwork/image-bootstrap/pull/90 233 | self._install_packages(['inetutils']) 234 | 235 | def get_cloud_init_datasource_cfg_path(self): 236 | return '/etc/cloud/cloud.cfg.d/90_datasource.cfg' 237 | 238 | def install_sshd(self): 239 | self._install_packages(['openssh']) 240 | 241 | def _make_services_autostart(self, service_names): 242 | for service_name in service_names: 243 | self._messenger.info('Making service "%s" start automatically...' % service_name) 244 | cmd = [ 245 | COMMAND_CHROOT, 246 | self._abs_mountpoint, 247 | 'systemctl', 248 | 'enable', 249 | service_name, 250 | ] 251 | self._executor.check_call(cmd, env=self.create_chroot_env()) 252 | 253 | def make_openstack_services_autostart(self): 254 | self._make_services_autostart([ 255 | 'systemd-networkd', 256 | 'systemd-resolved', # for nameserver IPs from DHCP 257 | 'sshd', 258 | 'cloud-init-main', 259 | 'cloud-init-local', 260 | 'cloud-init-network', 261 | 'cloud-config', 262 | 'cloud-final', 263 | ]) 264 | 265 | def get_vmlinuz_path(self): 266 | return '/boot/vmlinuz-linux' 267 | 268 | def get_initramfs_path(self): 269 | return '/boot/initramfs-linux.img' 270 | 271 | def install_kernel(self): 272 | self._install_packages(['linux']) 273 | 274 | def adjust_cloud_cfg_dict(self, cloud_cfg_dict): 275 | super(ArchStrategy, self).adjust_cloud_cfg_dict(cloud_cfg_dict) 276 | 277 | # Get rid of groups cdrom, dailout, dip, netdev, plugdev, sudo. 278 | # https://github.com/hartwork/image-bootstrap/issues/49#issuecomment-317191835 279 | # https://bugs.archlinux.org/task/54911 280 | system_info = cloud_cfg_dict.setdefault('system_info', {}) 281 | system_info__default_user = system_info.setdefault('default_user', {}) 282 | system_info__default_user['groups'] = ['adm'] 283 | 284 | def uses_systemd(self): 285 | return True 286 | 287 | def uses_systemd_resolved(self, with_openstack): 288 | return with_openstack 289 | 290 | def get_minimum_size_bytes(self): 291 | return 3 * 1024**3 292 | 293 | @classmethod 294 | def add_parser_to(clazz, distros): 295 | arch = distros.add_parser(clazz.DISTRO_KEY, help=clazz.DISTRO_NAME_LONG) 296 | arch.set_defaults(**{DISTRO_CLASS_FIELD: clazz}) 297 | 298 | ArchBootstrapper.add_arguments_to(arch) 299 | 300 | @classmethod 301 | def create(clazz, messenger, executor, options): 302 | return clazz( 303 | messenger, 304 | executor, 305 | os.path.abspath(options.cache_dir), 306 | options.image_date, 307 | options.mirror_url, 308 | os.path.abspath(options.resolv_conf), 309 | ) 310 | -------------------------------------------------------------------------------- /image_bootstrap/distros/base.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import os 7 | from abc import ABCMeta, abstractmethod 8 | 9 | import image_bootstrap.loaders._yaml as yaml 10 | from directory_bootstrap.shared.commands import COMMAND_CHROOT, COMMAND_WGET 11 | from image_bootstrap.engine import BOOTLOADER__CHROOT_GRUB2__DRIVE 12 | 13 | DISTRO_CLASS_FIELD = 'distro_class' 14 | 15 | 16 | class DistroStrategy(object, metaclass=ABCMeta): 17 | def __init__(self, messenger, executor, abs_cache_dir, abs_resolv_conf): 18 | self._messenger = messenger 19 | self._executor = executor 20 | 21 | self._abs_cache_dir = abs_cache_dir 22 | self._abs_resolv_conf = abs_resolv_conf 23 | 24 | def set_mountpoint(self, abs_mountpoint): 25 | self._abs_mountpoint = abs_mountpoint 26 | 27 | def set_chroot_env_prototype(self, chroot_env_prototype): 28 | self._chroot_env_prototype = chroot_env_prototype 29 | 30 | def create_chroot_env(self): 31 | return self._chroot_env_prototype.copy() 32 | 33 | def check_release(self): 34 | pass 35 | 36 | def select_bootloader(self): 37 | return BOOTLOADER__CHROOT_GRUB2__DRIVE 38 | 39 | def write_etc_hostname(self, hostname): 40 | filename = os.path.join(self._abs_mountpoint, 'etc', 'hostname') 41 | self._messenger.info('Writing file "%s"...' % filename) 42 | f = open(filename, 'w') 43 | print(hostname, file=f) 44 | f.close() 45 | 46 | @abstractmethod # leave calling write_etc_hostname to derived classes 47 | def configure_hostname(self, hostname): 48 | pass 49 | 50 | @abstractmethod 51 | def get_commands_to_check_for(self): 52 | pass 53 | 54 | def check_architecture(self, architecture): 55 | return architecture 56 | 57 | @abstractmethod 58 | def allow_autostart_of_services(self, allow): 59 | pass 60 | 61 | @abstractmethod 62 | def run_directory_bootstrap(self, architecture, bootloader_approach): 63 | pass 64 | 65 | @abstractmethod 66 | def create_network_configuration(self, use_mtu_tristate): 67 | pass 68 | 69 | @abstractmethod 70 | def ensure_chroot_has_grub2_installed(self): 71 | pass 72 | 73 | @abstractmethod 74 | def get_chroot_command_grub2_install(self): 75 | pass 76 | 77 | @abstractmethod 78 | def generate_grub_cfg_from_inside_chroot(self): 79 | pass 80 | 81 | def adjust_initramfs_generator_config(self): 82 | pass 83 | 84 | @abstractmethod 85 | def generate_initramfs_from_inside_chroot(self): 86 | pass 87 | 88 | @abstractmethod 89 | def perform_in_chroot_shipping_clean_up(self): 90 | pass 91 | 92 | @abstractmethod 93 | def perform_post_chroot_clean_up(self): 94 | pass 95 | 96 | def get_cloud_username(self): 97 | return self.DISTRO_KEY 98 | 99 | def get_cloud_init_distro(self): 100 | return self.DISTRO_KEY 101 | 102 | @abstractmethod 103 | def install_dhcp_client(self): 104 | pass 105 | 106 | @abstractmethod 107 | def install_sudo(self): 108 | pass 109 | 110 | @abstractmethod 111 | def install_cloud_init_and_friends(self): 112 | pass 113 | 114 | @abstractmethod 115 | def get_cloud_init_datasource_cfg_path(self): 116 | pass 117 | 118 | @abstractmethod 119 | def install_sshd(self): 120 | pass 121 | 122 | @abstractmethod 123 | def make_openstack_services_autostart(self): 124 | pass 125 | 126 | @abstractmethod 127 | def get_vmlinuz_path(self): 128 | pass 129 | 130 | @abstractmethod 131 | def get_initramfs_path(self): 132 | pass 133 | 134 | def prepare_installation_of_packages(self): 135 | pass 136 | 137 | @abstractmethod 138 | def install_kernel(self): 139 | pass 140 | 141 | def _fetch_install_chmod(self, url, local_path, permissions): 142 | full_local_path = os.path.join(self._abs_mountpoint, local_path.lstrip('/')) 143 | cmd = [ 144 | COMMAND_WGET, 145 | '-O%s' % full_local_path, 146 | url, 147 | ] 148 | self._executor.check_call(cmd) 149 | os.chmod(full_local_path, permissions) 150 | 151 | def install_growpart(self): 152 | self._messenger.info('Fetching growpart of cloud-utils...') 153 | self._fetch_install_chmod( 154 | 'https://raw.githubusercontent.com/canonical/cloud-utils/0.31/bin/growpart', 155 | '/usr/bin/growpart', 0o755) 156 | 157 | def disable_cloud_init_syslog_fix_perms(self): 158 | # https://github.com/hartwork/image-bootstrap/issues/17 159 | filename = os.path.join(self._abs_mountpoint, 'etc/cloud/cloud.cfg.d/00_syslog_fix_perms.cfg') 160 | self._messenger.info('Writing file "%s"...' % filename) 161 | with open(filename, 'w') as f: 162 | print('syslog_fix_perms: null', file=f) 163 | 164 | def adjust_cloud_cfg_dict(self, cloud_cfg_dict): 165 | system_info = cloud_cfg_dict.setdefault('system_info', {}) 166 | 167 | system_info__default_user = system_info.setdefault('default_user', {}) 168 | system_info__default_user['name'] = self.get_cloud_username() 169 | system_info__default_user['gecos'] = 'Cloud-init-user' 170 | system_info__default_user.setdefault('sudo', 171 | ['ALL=(ALL) NOPASSWD:ALL']) 172 | 173 | system_info['distro'] = self.get_cloud_init_distro() 174 | 175 | def adjust_etc_cloud_cfg(self): 176 | filename = os.path.join(self._abs_mountpoint, 'etc/cloud/cloud.cfg') 177 | self._messenger.info('Adjusting file "%s"...' % filename) 178 | with open(filename, 'r') as f: 179 | d = yaml.safe_load(f.read()) 180 | self.adjust_cloud_cfg_dict(d) 181 | with open(filename, 'w') as f: 182 | print('# Re-written by image-bootstrap', file=f) 183 | print(yaml.safe_dump(d, default_flow_style=False), file=f) 184 | 185 | @abstractmethod 186 | def uses_systemd(self): 187 | pass 188 | 189 | @abstractmethod 190 | def uses_systemd_resolved(self, with_openstack): 191 | pass 192 | 193 | @abstractmethod 194 | def get_minimum_size_bytes(self): 195 | pass 196 | 197 | def _ensure_eth0_naming(self): 198 | etc_default_grub = os.path.join(self._abs_mountpoint, 'etc/default/grub') 199 | self._messenger.info('Adjusting file "%s"...' % etc_default_grub) 200 | self._executor.check_call([ 201 | COMMAND_CHROOT, self._abs_mountpoint, 202 | 'sed', 203 | 's,#\\?GRUB_CMDLINE_LINUX=.*",GRUB_CMDLINE_LINUX="net.ifnames=0" # set by image-bootstrap,', 204 | '-i', '/etc/default/grub', 205 | ], env=self.create_chroot_env()) 206 | 207 | def adjust_grub_defaults(self, with_openstack): 208 | pass 209 | 210 | def install_acpid(self): 211 | # NOTE: Only called for distros NOT using systemd 212 | raise NotImplementedError() 213 | 214 | def get_extra_mkfs_ext4_options(self): 215 | return [] 216 | 217 | @classmethod 218 | def add_parser_to(clazz, distros): 219 | raise NotImplementedError() 220 | 221 | @classmethod 222 | def create(clazz, messenger, executor, options): 223 | raise NotImplementedError() 224 | -------------------------------------------------------------------------------- /image_bootstrap/distros/debian.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | from image_bootstrap.distros.debian_based import DebianBasedDistroStrategy 7 | 8 | 9 | class DebianStrategy(DebianBasedDistroStrategy): 10 | DISTRO_KEY = 'debian' 11 | DISTRO_NAME_SHORT = 'Debian' 12 | DISTRO_NAME_LONG = 'Debian GNU/Linux' 13 | DEFAULT_RELEASE = 'bookworm' 14 | DEFAULT_MIRROR_URL = 'http://httpredir.debian.org/debian' 15 | APT_CACHER_NG_URL = 'http://localhost:3142/debian' 16 | 17 | def check_release(self): 18 | if self._release in ('stable', 'testing'): 19 | raise ValueError('For Debian releases, please use names like "%s" rather than "%s".' 20 | % (self.DEFAULT_RELEASE, self._release)) 21 | 22 | if self._release in ('wheezy', 'jessie', 'stretch', 'buster'): 23 | raise ValueError('Release "%s" is no longer supported.' % self._release) 24 | 25 | def get_kernel_package_name(self, architecture): 26 | if architecture == 'i386': 27 | return 'linux-image-686-pae' 28 | 29 | return 'linux-image-%s' % architecture 30 | 31 | def install_cloud_init_and_friends(self): 32 | self._install_packages(['cloud-init', 'cloud-utils', 'cloud-initramfs-growroot']) 33 | 34 | def uses_systemd(self): 35 | return True 36 | 37 | def uses_systemd_resolved(self, with_openstack): 38 | return False 39 | 40 | def get_minimum_size_bytes(self): 41 | return 2 * 1024**3 42 | 43 | def get_extra_mkfs_ext4_options(self): 44 | args = super(DebianStrategy, self).get_extra_mkfs_ext4_options() 45 | args += ['-O', '^metadata_csum'] 46 | return args 47 | -------------------------------------------------------------------------------- /image_bootstrap/distros/debian_based.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import errno 7 | import os 8 | import subprocess 9 | from abc import ABCMeta, abstractmethod 10 | from textwrap import dedent 11 | 12 | from directory_bootstrap.shared.commands import ( 13 | COMMAND_FIND, COMMAND_UNAME, COMMAND_UNSHARE) 14 | from image_bootstrap.distros.base import DISTRO_CLASS_FIELD, DistroStrategy 15 | from image_bootstrap.engine import ( 16 | BOOTLOADER__ANY_GRUB, BOOTLOADER__HOST_EXTLINUX, COMMAND_CHROOT) 17 | 18 | 19 | _ETC_NETWORK_INTERFACES_CONTENT = """\ 20 | # This file describes the network interfaces available on your system 21 | # and how to activate them. For more information, see interfaces(5). 22 | 23 | source /etc/network/interfaces.d/* 24 | 25 | # The loopback network interface 26 | auto lo 27 | iface lo inet loopback 28 | 29 | # The primary network interface 30 | allow-hotplug eth0 31 | iface eth0 inet dhcp 32 | """ 33 | 34 | 35 | class _ArchitectureMachineMismatch(Exception): 36 | def __init__(self, architecture, machine): 37 | self._architecture = architecture 38 | self._machine = machine 39 | 40 | def __str__(self): 41 | return 'Bootstrapping architecture %s on %s machines not supported' \ 42 | % (self._architecture, self._machine) 43 | 44 | 45 | class DebianBasedDistroStrategy(DistroStrategy, metaclass=ABCMeta): 46 | def __init__(self, 47 | messenger, 48 | executor, 49 | 50 | release, 51 | mirror_url, 52 | command_debootstrap, 53 | debootstrap_opt, 54 | ): 55 | self._messenger = messenger 56 | self._executor = executor 57 | 58 | self._release = release 59 | self._mirror_url = mirror_url 60 | self._command_debootstrap = command_debootstrap 61 | self._debootstrap_opt = debootstrap_opt 62 | 63 | @abstractmethod 64 | def check_release(self): 65 | pass 66 | 67 | def get_commands_to_check_for(self): 68 | return [ 69 | COMMAND_CHROOT, 70 | COMMAND_FIND, 71 | COMMAND_UNAME, 72 | COMMAND_UNSHARE, 73 | self._command_debootstrap, 74 | ] 75 | 76 | @abstractmethod 77 | def get_kernel_package_name(self, architecture): 78 | pass 79 | 80 | def check_architecture(self, architecture): 81 | uname_output = subprocess.check_output([COMMAND_UNAME, '-m']) 82 | host_machine = uname_output.rstrip().decode('utf-8') 83 | 84 | trouble = False 85 | if architecture == 'amd64' and host_machine != 'x86_64': 86 | trouble = True 87 | elif architecture == 'i386': 88 | if host_machine not in ('i386', 'i486', 'i586', 'i686', 'x86_64'): 89 | trouble = True 90 | 91 | if trouble: 92 | raise _ArchitectureMachineMismatch(architecture, host_machine) 93 | 94 | return architecture 95 | 96 | def configure_hostname(self, hostname): 97 | self.write_etc_hostname(hostname) 98 | 99 | def allow_autostart_of_services(self, allow): 100 | policy_rc_d_path = os.path.join(self._abs_mountpoint, 'usr/sbin/policy-rc.d') 101 | 102 | verb_activate = 'Re-activating' if allow else 'Deactivating' 103 | verb_create = 'removing' if allow else 'writing' 104 | self._messenger.info('%s auto-starting of services from package installations (by %s file "%s")...' 105 | % (verb_activate, verb_create, policy_rc_d_path)) 106 | 107 | if allow: 108 | try: 109 | os.remove(policy_rc_d_path) 110 | except OSError as e: 111 | if e.errno != errno.ENOENT: 112 | raise 113 | else: 114 | # https://people.debian.org/~hmh/invokerc.d-policyrc.d-specification.txt 115 | with open(policy_rc_d_path, 'w') as f: 116 | print(dedent("""\ 117 | #! /bin/sh 118 | exit 101 119 | """), file=f) 120 | os.fchmod(f.fileno(), 0o755) 121 | 122 | def run_directory_bootstrap(self, architecture, bootloader_approach): 123 | self._messenger.info('Bootstrapping %s "%s" into "%s"...' 124 | % (self.DISTRO_NAME_SHORT, self._release, self._abs_mountpoint)) 125 | 126 | _extra_packages = [ 127 | 'initramfs-tools', # for update-initramfs 128 | self.get_kernel_package_name(architecture), 129 | ] 130 | if bootloader_approach in BOOTLOADER__ANY_GRUB: 131 | _extra_packages.append('grub-pc') 132 | elif bootloader_approach == BOOTLOADER__HOST_EXTLINUX: 133 | pass 134 | else: 135 | raise NotImplementedError('Unsupported bootloader for %s' % self.DISTRO_NAME_SHORT) 136 | 137 | cmd = [ 138 | COMMAND_UNSHARE, 139 | '--mount', 140 | '--', 141 | self._command_debootstrap, 142 | '--arch', architecture, 143 | '--include=%s' % ','.join(_extra_packages), 144 | ] \ 145 | + self._debootstrap_opt \ 146 | + [ 147 | self._release, 148 | self._abs_mountpoint, 149 | self._mirror_url, 150 | ] 151 | self._executor.check_call(cmd) 152 | 153 | def create_network_configuration(self, use_mtu_tristate): 154 | filename = os.path.join(self._abs_mountpoint, 'etc', 'network', 'interfaces') 155 | self._messenger.info('Writing file "%s"...' % filename) 156 | f = open(filename, 'w') 157 | print(_ETC_NETWORK_INTERFACES_CONTENT, file=f) 158 | f.close() 159 | 160 | # TODO For non-None use_mtu_tristate, force DHCP client option 26/interface-mtu 161 | use_mtu_tristate 162 | 163 | def ensure_chroot_has_grub2_installed(self): 164 | pass # debootstrap has already pulled GRUB 2.x in 165 | 166 | def get_chroot_command_grub2_install(self): 167 | return 'grub-install' 168 | 169 | def generate_grub_cfg_from_inside_chroot(self): 170 | cmd = [ 171 | COMMAND_CHROOT, 172 | self._abs_mountpoint, 173 | 'update-grub', 174 | ] 175 | self._executor.check_call(cmd, env=self.create_chroot_env()) 176 | 177 | def generate_initramfs_from_inside_chroot(self): 178 | cmd = [ 179 | COMMAND_CHROOT, 180 | self._abs_mountpoint, 181 | 'update-initramfs', 182 | '-u', 183 | '-k', 'all', 184 | ] 185 | self._executor.check_call(cmd, env=self.create_chroot_env()) 186 | 187 | def perform_in_chroot_shipping_clean_up(self): 188 | pass # nothing, yet 189 | 190 | def perform_post_chroot_clean_up(self): 191 | self._messenger.info('Cleaning chroot apt cache...') 192 | cmd = [ 193 | COMMAND_FIND, 194 | os.path.join(self._abs_mountpoint, 'var', 'cache', 'apt', 'archives'), 195 | '-type', 'f', 196 | '-name', '*.deb', 197 | '-delete', 198 | ] 199 | self._executor.check_call(cmd) 200 | 201 | def _install_packages(self, package_names): 202 | self._messenger.info('Installing %s...' % ', '.join(package_names)) 203 | env = self.create_chroot_env() 204 | env.setdefault('DEBIAN_FRONTEND', 'noninteractive') 205 | cmd = [ 206 | COMMAND_CHROOT, 207 | self._abs_mountpoint, 208 | 'apt-get', 209 | 'install', 210 | '-y', '--no-install-recommends', '-V', 211 | ] + list(package_names) 212 | self._executor.check_call(cmd, env=env) 213 | 214 | def install_dhcp_client(self): 215 | pass # already installed 216 | 217 | def install_sudo(self): 218 | self._install_packages(['sudo']) 219 | 220 | @abstractmethod 221 | def install_cloud_init_and_friends(self): 222 | pass 223 | 224 | def get_cloud_init_datasource_cfg_path(self): 225 | return '/etc/cloud/cloud.cfg.d/90_dpkg.cfg' # existing file 226 | 227 | def install_sshd(self): 228 | self._install_packages(['openssh-server']) 229 | 230 | def make_openstack_services_autostart(self): 231 | pass # autostarted in Debian, already 232 | 233 | def get_vmlinuz_path(self): 234 | return '/vmlinuz' 235 | 236 | def get_initramfs_path(self): 237 | return '/initrd.img' 238 | 239 | def install_kernel(self): 240 | pass # Kernel installed, already 241 | 242 | def adjust_grub_defaults(self, with_openstack): 243 | self._ensure_eth0_naming() 244 | 245 | def install_acpid(self): 246 | self._install_packages(['acpid']) 247 | 248 | @classmethod 249 | def add_parser_to(clazz, distros): 250 | debian = distros.add_parser(clazz.DISTRO_KEY, help=clazz.DISTRO_NAME_LONG) 251 | debian.set_defaults(**{DISTRO_CLASS_FIELD: clazz}) 252 | 253 | debian_commands = debian.add_argument_group('command names') 254 | debian_commands.add_argument('--debootstrap', metavar='COMMAND', 255 | dest='command_debootstrap', default='debootstrap', 256 | help='override debootstrap command') 257 | 258 | debian.add_argument('--release', dest='release', default=clazz.DEFAULT_RELEASE, 259 | metavar='RELEASE', 260 | help='specify %s release (default: %%(default)s)' 261 | % clazz.DISTRO_NAME_SHORT) 262 | debian.add_argument('--mirror', dest='mirror_url', metavar='URL', 263 | default=clazz.DEFAULT_MIRROR_URL, 264 | help='specify %s mirror to use (e.g. %s for ' 265 | 'a local instance of apt-cacher-ng; default: %%(default)s)' 266 | % (clazz.DISTRO_NAME_SHORT, clazz.APT_CACHER_NG_URL)) 267 | 268 | debian.add_argument('--debootstrap-opt', dest='debootstrap_opt', 269 | metavar='OPTION', action='append', default=[], 270 | help='option to pass to debootstrap, in addition; ' 271 | 'can be passed several times; ' 272 | 'use with --debootstrap-opt=... syntax, i.e. with "="') 273 | 274 | @classmethod 275 | def create(clazz, messenger, executor, options): 276 | return clazz( 277 | messenger, 278 | executor, 279 | options.release, 280 | options.mirror_url, 281 | options.command_debootstrap, 282 | options.debootstrap_opt, 283 | ) 284 | -------------------------------------------------------------------------------- /image_bootstrap/distros/ubuntu.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import os 5 | 6 | from image_bootstrap.distros.debian_based import DebianBasedDistroStrategy 7 | from image_bootstrap.engine import BOOTLOADER__HOST_EXTLINUX 8 | 9 | 10 | class UbuntuStrategy(DebianBasedDistroStrategy): 11 | DISTRO_KEY = 'ubuntu' 12 | DISTRO_NAME_SHORT = 'Ubuntu' 13 | DISTRO_NAME_LONG = 'Ubuntu' 14 | DEFAULT_RELEASE = 'trusty' 15 | DEFAULT_MIRROR_URL = 'http://archive.ubuntu.com/ubuntu' 16 | APT_CACHER_NG_URL = 'http://localhost:3142/ubuntu' 17 | 18 | def select_bootloader(self): 19 | return BOOTLOADER__HOST_EXTLINUX 20 | 21 | def check_release(self): 22 | pass 23 | 24 | def get_kernel_package_name(self, architecture): 25 | return 'linux-image-generic' 26 | 27 | def adjust_grub_defaults(self, with_openstack): 28 | super(UbuntuStrategy, self).adjust_grub_defaults(with_openstack) 29 | 30 | subst = ( 31 | ('GRUB_TIMEOUT=', 'GRUB_TIMEOUT=1'), 32 | ('GRUB_HIDDEN_TIMEOUT', None), 33 | ) 34 | etc_default_grub = os.path.join(self._abs_mountpoint, 'etc/default/grub') 35 | with open(etc_default_grub, 'r') as f: 36 | content = f.read() 37 | 38 | lines_to_write = [] 39 | for line in content.split('\n'): 40 | for prefix, replacement in subst: 41 | if line.startswith(prefix): 42 | if replacement is None: 43 | line = '## ' + line 44 | else: 45 | line = replacement 46 | lines_to_write.append(line) 47 | 48 | self._messenger.info('Adjusting file "%s"...' % etc_default_grub) 49 | with open(etc_default_grub, 'w') as f: 50 | f.write('\n'.join(lines_to_write)) 51 | 52 | def install_cloud_init_and_friends(self): 53 | # Do not install cloud-initramfs-growroot (from universe) 54 | # if cloud-init and growpart alone work just fine 55 | self._install_packages(['cloud-init', 'cloud-utils']) 56 | 57 | def uses_systemd(self): 58 | # NOTE: assumes not supporting anything older than trusty 59 | return self._release != 'trusty' 60 | 61 | def uses_systemd_resolved(self, with_openstack): 62 | return False 63 | 64 | def get_minimum_size_bytes(self): 65 | return 2 * 1024**3 66 | -------------------------------------------------------------------------------- /image_bootstrap/loaders/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/image_bootstrap/loaders/__init__.py -------------------------------------------------------------------------------- /image_bootstrap/loaders/_yaml.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import sys 7 | 8 | try: 9 | from yaml import safe_dump, safe_load 10 | except ImportError: 11 | print('ERROR: Please install PyYAML ' 12 | '(https://pypi.python.org/pypi/PyYAML). ' 13 | 'Thank you!', file=sys.stderr) 14 | sys.exit(1) 15 | 16 | # Mark as used 17 | safe_dump 18 | safe_load 19 | 20 | del sys 21 | -------------------------------------------------------------------------------- /image_bootstrap/mount.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | 5 | 6 | import os 7 | import re 8 | 9 | # See https://www.kernel.org/doc/Documentation/filesystems/proc.txt 10 | _PROC_PID_MOUNTINFO_LINE = re.compile( 11 | '^(?P[0-9]+) ' 12 | '(?P[0-9]+) ' 13 | '(?P[0-9]+):(?P[0-9]+) ' 14 | '(?P(?:/|mnt:|net:)[^ ]*) ' 15 | '(?P/[^ ]*) ' # Spaces are encoded as "\040" 16 | '.+$') 17 | 18 | 19 | class MountFinder(object): 20 | def __init__(self): 21 | self._mount_points = [] 22 | 23 | @staticmethod 24 | def _parse_line(line): 25 | assert '\n' not in line 26 | match = _PROC_PID_MOUNTINFO_LINE.match(line) 27 | if match is None: 28 | raise ValueError(f'Unexpected line format: {line!r}') 29 | return match.groupdict() 30 | 31 | def _load_text(self, text): 32 | for line in text.split('\n'): 33 | if not line: 34 | continue 35 | self._mount_points.append(self._parse_line(line)['mount']) 36 | 37 | def load(self, filename=None): 38 | if filename is None: 39 | filename = '/proc/%d/mountinfo' % os.getpid() 40 | 41 | with open(filename, 'r') as f: 42 | self._load_text(f.read()) 43 | 44 | def _normpath_no_trailing_slash(self, abs_path): 45 | return os.path.normpath(abs_path) 46 | 47 | def _normpath_trailing_slash(self, abs_path): 48 | return os.path.join(os.path.normpath(abs_path), '') 49 | 50 | def below(self, abs_path, inclusive=False): 51 | prefix = self._normpath_trailing_slash(abs_path) 52 | for abs_candidate in self._mount_points: 53 | normed_candidate = self._normpath_trailing_slash(abs_candidate) 54 | if normed_candidate.startswith(prefix): 55 | if normed_candidate == prefix and not inclusive: 56 | continue 57 | yield self._normpath_no_trailing_slash(normed_candidate) 58 | -------------------------------------------------------------------------------- /image_bootstrap/test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/image_bootstrap/test/__init__.py -------------------------------------------------------------------------------- /image_bootstrap/test/test_mount.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from image_bootstrap.mount import MountFinder 4 | 5 | 6 | class TestMountInfoParser(TestCase): 7 | 8 | def test_line_parsing(self): 9 | for mount_info_line, expected_mount in ( 10 | ('17 21 0:4 / /proc rw,nosuid,nodev,noexec,relatime shared:12 - proc proc rw', '/proc'), 11 | ('314 20 0:3 net:[4026532205] /run/docker/netns/8546120315b2 rw shared:124 - nsfs nsfs rw', '/run/docker/netns/8546120315b2'), 12 | ('671 491 0:4 mnt:[4026532218] /run/snapd/ns/lxd.mnt rw - nsfs nsfs rw', '/run/snapd/ns/lxd.mnt'), 13 | ): 14 | finder = MountFinder() 15 | groupdict = finder._parse_line(mount_info_line) 16 | assert groupdict['mount'] == expected_mount 17 | 18 | def test_loading(self): 19 | finder = MountFinder() 20 | finder.load() 21 | -------------------------------------------------------------------------------- /image_bootstrap/types/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hartwork/image-bootstrap/5186edf91dd5c091639c604fe2fc82e391b3efa3/image_bootstrap/types/__init__.py -------------------------------------------------------------------------------- /image_bootstrap/types/disk_id.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import re 5 | 6 | _DISK_ID_PATTERN = '^0x[0-9a-fA-F]{1,8}$' 7 | _DISK_ID_MATCHER = re.compile(_DISK_ID_PATTERN) 8 | 9 | 10 | def _hex_string_to_number(text): 11 | if not _DISK_ID_MATCHER.match(text): 12 | raise ValueError('"%s" does not match pattern "%s"' % (text, _DISK_ID_PATTERN)) 13 | 14 | return int(text, 16) 15 | 16 | 17 | class DiskIdentifier(object): 18 | def __init__(self, number): 19 | self._number = number 20 | 21 | def __str__(self): 22 | return '0x%8x' % self._number 23 | 24 | def byte_sequence(self): 25 | return ''.join([chr((self._number >> i * 8) & 255) for i in range(4)]) 26 | 27 | 28 | def disk_id_type(text): 29 | """ 30 | Meant to be used as an argparse type 31 | """ 32 | return DiskIdentifier(_hex_string_to_number(text)) 33 | 34 | 35 | disk_id_type.__name__ = 'disk identifier' 36 | -------------------------------------------------------------------------------- /image_bootstrap/types/machine_id.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import re 5 | 6 | _MACHINE_ID_PATTERN = '^[0-9a-f]{32}$' 7 | _MACHINE_ID_MATCHER = re.compile(_MACHINE_ID_PATTERN) 8 | 9 | 10 | def machine_id_type(text): 11 | """ 12 | Meant to be used as an argparse type 13 | """ 14 | if not _MACHINE_ID_MATCHER.match(text): 15 | raise ValueError('"%s" does not match pattern "%s"' % (text, _MACHINE_ID_PATTERN)) 16 | return text 17 | 18 | 19 | machine_id_type.__name__ = 'machine identifier' 20 | -------------------------------------------------------------------------------- /image_bootstrap/types/uuid.py: -------------------------------------------------------------------------------- 1 | # Copyright (C) 2015 Sebastian Pipping 2 | # Licensed under AGPL v3 or later 3 | 4 | import re 5 | 6 | _UUID_PATTERN = '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$' 7 | _SANE_UUID_CHECKER = re.compile(_UUID_PATTERN) 8 | 9 | 10 | def require_valid_uuid(text): 11 | if not _SANE_UUID_CHECKER.match(text): 12 | raise ValueError('Not a well-formed UUID: "%s"' % text) 13 | 14 | 15 | def uuid_type(text): 16 | """ 17 | Meant to be used as an argparse type 18 | """ 19 | require_valid_uuid(text) 20 | return text 21 | 22 | 23 | uuid_type.__name__ = 'UUID' 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # Direct 2 | beautifulsoup4==4.13.4 3 | colorama==0.4.6 4 | coverage==7.8.2 5 | lxml==5.4.0 6 | pytest==8.4.0 7 | PyYAML==6.0.2 8 | requests==2.32.3 9 | 10 | # Indirect 11 | attrs==25.3.0 12 | certifi==2025.4.26 13 | charset-normalizer==3.4.2 14 | exceptiongroup==1.3.0 15 | idna==3.10 16 | importlib_metadata==8.7.0 17 | iniconfig==2.1.0 18 | packaging==25.0 19 | pluggy==1.6.0 20 | Pygments==2.19.1 21 | pyparsing==3.2.3 22 | soupsieve==2.7 23 | tomli==2.2.1 24 | typing_extensions==4.13.2 25 | urllib3==2.4.0 26 | zipp==3.22.0 27 | -------------------------------------------------------------------------------- /scripts/debug.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright (C) 2015 Sebastian Pipping 3 | # Licensed under AGPL v3 or later 4 | 5 | echo XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 6 | echo "Called as:" 7 | echo " \$0 = \"$0"\" 8 | echo " \$@ = <$@>" 9 | echo XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 10 | echo Environment: 11 | env | sort 12 | echo XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 13 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python3 2 | # Copyright (C) 2015 Sebastian Pipping 3 | # Licensed under AGPL v3 or later 4 | 5 | import glob 6 | import os 7 | 8 | from setuptools import find_packages, setup 9 | 10 | from directory_bootstrap.shared.metadata import ( 11 | GITHUB_HOME_URL, PACKAGE_NAME, VERSION_STR) 12 | 13 | _tests_require = [ 14 | 'pytest', 15 | ] 16 | 17 | _extras_require = { 18 | 'tests': _tests_require, 19 | } 20 | 21 | 22 | if __name__ == '__main__': 23 | setup( 24 | name=PACKAGE_NAME, 25 | description='Command line tool for creating bootable virtual machine images', 26 | long_description=open('README.md').read(), 27 | long_description_content_type='text/markdown', 28 | license='AGPL v3 or later', 29 | version=VERSION_STR, 30 | author='Sebastian Pipping', 31 | author_email='sebastian@pipping.org', 32 | url=GITHUB_HOME_URL, 33 | python_requires='>=3.9', 34 | setup_requires=[ 35 | 'setuptools>=38.6.0', # for long_description_content_type 36 | ], 37 | install_requires=[ 38 | 'beautifulsoup4', 39 | 'colorama', 40 | 'lxml', 41 | 'requests', 42 | 'setuptools', 43 | 'PyYAML', 44 | ], 45 | tests_require=_tests_require, 46 | extras_require=_extras_require, 47 | packages=[ 48 | p for p in find_packages() if not p.endswith('.test') 49 | ], 50 | package_data={ 51 | 'directory_bootstrap': [ 52 | 'resources/alpine/ncopa.asc', 53 | ] + [ 54 | os.path.relpath(p, 'directory_bootstrap') 55 | for p 56 | in glob.glob('directory_bootstrap/resources/*/*.asc') 57 | ], 58 | }, 59 | entry_points={ 60 | 'console_scripts': [ 61 | 'directory-bootstrap = directory_bootstrap.__main__:main', 62 | 'image-bootstrap = image_bootstrap.__main__:main', 63 | ], 64 | }, 65 | classifiers=[ 66 | 'Development Status :: 4 - Beta', 67 | 'Environment :: Console', 68 | 'Environment :: OpenStack', 69 | 'Intended Audience :: Developers', 70 | 'Intended Audience :: End Users/Desktop', 71 | 'Intended Audience :: System Administrators', 72 | 'License :: OSI Approved :: GNU Affero General Public License v3 or later (AGPLv3+)', 73 | 'Natural Language :: English', 74 | 'Operating System :: POSIX :: Linux', 75 | 'Programming Language :: Python', 76 | 'Programming Language :: Python :: 3', 77 | 'Programming Language :: Python :: 3.9', 78 | 'Programming Language :: Python :: 3.10', 79 | 'Programming Language :: Python :: 3.11', 80 | 'Programming Language :: Python :: 3.12', 81 | 'Programming Language :: Python :: 3.13', 82 | 'Programming Language :: Python :: 3 :: Only', 83 | 'Topic :: System :: Installation/Setup', 84 | 'Topic :: Utilities', 85 | ], 86 | ) 87 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | # Copyright (C) 2015 Sebastian Pipping 3 | # Licensed under AGPL v3 or later 4 | 5 | on_exit() { 6 | echo FAILED 1>&2 7 | } 8 | trap on_exit exit 9 | 10 | 11 | FAIL_USAGE() { 12 | echo "USAGE: $(basename "$0") BLOCK_DEV ROOT_PASSWORD" 1>&2 13 | exit 1 14 | } 15 | 16 | 17 | DEBIAN_MIRROR_URL=http://ftp.de.debian.org/debian/ 18 | HTTP_PROXY_URL=http://127.0.0.1:8123/ # polipo 19 | 20 | 21 | if [[ ! -b "$1" ]]; then 22 | FAIL_USAGE 23 | fi 24 | target_device="$1"; shift 25 | 26 | if [[ -z "$1" ]]; then 27 | FAIL_USAGE 28 | fi 29 | root_password="$1"; shift 30 | 31 | 32 | if [[ "$(id -u)" -ne 0 ]]; then 33 | echo "ERROR: Yo do not seem to be root (user ID 0)." 1>&2 34 | exit 1 35 | fi 36 | 37 | 38 | ECHO_RUN() { 39 | echo '#' "$@" 40 | "$@" 41 | } 42 | 43 | BUILD() { 44 | name="$1"; shift 45 | ECHO_RUN env http_proxy="${HTTP_PROXY_URL}" ./image-bootstrap --verbose --password "${root_password}" "$@" "${target_device}" 46 | } 47 | 48 | 49 | set -e 50 | 51 | 52 | BUILD arch-openstack \ 53 | --openstack arch 54 | BUILD debian-stretch-openstack \ 55 | --openstack debian --release stretch --mirror "${DEBIAN_MIRROR_URL}" 56 | BUILD ubuntu-vivid \ 57 | ubuntu --release vivid 58 | 59 | 60 | trap - exit 61 | echo PASSED 62 | --------------------------------------------------------------------------------