2019-12-14 14:12:55 -05:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
# debootstrap_script.py - PVC Provisioner example script for Debootstrap
|
|
|
|
# Part of the Parallel Virtual Cluster (PVC) system
|
|
|
|
#
|
2021-03-25 17:01:55 -04:00
|
|
|
# Copyright (C) 2018-2021 Joshua M. Boniface <joshua@boniface.me>
|
2019-12-14 14:12:55 -05:00
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
2021-03-25 16:57:17 -04:00
|
|
|
# the Free Software Foundation, version 3.
|
2019-12-14 14:12:55 -05:00
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
#
|
|
|
|
###############################################################################
|
|
|
|
|
|
|
|
# This script provides an example of a PVC provisioner script. It will install
|
|
|
|
# a Debian system, of the release specified in the keyword argument `deb_release`
|
|
|
|
# and from the mirror specified in the keyword argument `deb_mirror`, and
|
|
|
|
# including the packages specified in the keyword argument `deb_packages` (a list
|
|
|
|
# of strings, which is then joined together as a CSV and passed to debootstrap),
|
|
|
|
# to the configured disks, configure fstab, and install GRUB. Any later config
|
|
|
|
# should be done within the VM, for instance via cloud-init.
|
|
|
|
|
|
|
|
# This script can thus be used as an example or reference implementation of a
|
|
|
|
# PVC provisioner script and expanded upon as required.
|
|
|
|
|
|
|
|
# This script will run under root privileges as the provisioner does. Be careful
|
|
|
|
# with that.
|
|
|
|
|
|
|
|
import os
|
2021-07-11 23:10:41 -04:00
|
|
|
from contextlib import contextmanager
|
|
|
|
|
|
|
|
|
|
|
|
# Create a chroot context manager
|
|
|
|
# This can be used later in the script to chroot to the destination directory
|
|
|
|
# for instance to run commands within the target.
|
|
|
|
@contextmanager
|
|
|
|
def chroot_target(destination):
|
|
|
|
try:
|
|
|
|
real_root = os.open("/", os.O_RDONLY)
|
|
|
|
os.chroot(destination)
|
|
|
|
fake_root = os.open("/", os.O_RDONLY)
|
|
|
|
os.fchdir(fake_root)
|
|
|
|
yield
|
|
|
|
finally:
|
|
|
|
os.fchdir(real_root)
|
|
|
|
os.chroot(".")
|
|
|
|
os.fchdir(real_root)
|
|
|
|
os.close(fake_root)
|
|
|
|
os.close(real_root)
|
|
|
|
del fake_root
|
|
|
|
del real_root
|
|
|
|
|
2019-12-14 14:12:55 -05:00
|
|
|
|
|
|
|
# Installation function - performs a debootstrap install of a Debian system
|
|
|
|
# Note that the only arguments are keyword arguments.
|
|
|
|
def install(**kwargs):
|
|
|
|
# The provisioner has already mounted the disks on kwargs['temporary_directory'].
|
|
|
|
# by this point, so we can get right to running the debootstrap after setting
|
|
|
|
# some nicer variable names; you don't necessarily have to do this.
|
2021-11-06 03:02:43 -04:00
|
|
|
vm_name = kwargs["vm_name"]
|
|
|
|
temporary_directory = kwargs["temporary_directory"]
|
|
|
|
disks = kwargs["disks"]
|
|
|
|
networks = kwargs["networks"]
|
2019-12-14 14:12:55 -05:00
|
|
|
# Our own required arguments. We should, though are not required to, handle
|
|
|
|
# failures of these gracefully, should administrators forget to specify them.
|
|
|
|
try:
|
2021-11-06 03:02:43 -04:00
|
|
|
deb_release = kwargs["deb_release"]
|
2020-11-06 19:24:10 -05:00
|
|
|
except Exception:
|
2019-12-14 14:12:55 -05:00
|
|
|
deb_release = "stable"
|
|
|
|
try:
|
2021-11-06 03:02:43 -04:00
|
|
|
deb_mirror = kwargs["deb_mirror"]
|
2020-11-06 19:24:10 -05:00
|
|
|
except Exception:
|
2019-12-14 14:12:55 -05:00
|
|
|
deb_mirror = "http://ftp.debian.org/debian"
|
|
|
|
try:
|
2021-11-06 03:02:43 -04:00
|
|
|
deb_packages = kwargs["deb_packages"].split(",")
|
2020-11-06 19:24:10 -05:00
|
|
|
except Exception:
|
2021-11-06 03:02:43 -04:00
|
|
|
deb_packages = [
|
|
|
|
"linux-image-amd64",
|
|
|
|
"grub-pc",
|
|
|
|
"cloud-init",
|
|
|
|
"python3-cffi-backend",
|
|
|
|
"wget",
|
|
|
|
]
|
2019-12-14 14:12:55 -05:00
|
|
|
|
|
|
|
# We need to know our root disk
|
|
|
|
root_disk = None
|
|
|
|
for disk in disks:
|
2021-11-06 03:02:43 -04:00
|
|
|
if disk["mountpoint"] == "/":
|
2019-12-14 14:12:55 -05:00
|
|
|
root_disk = disk
|
|
|
|
if not root_disk:
|
|
|
|
return
|
|
|
|
|
|
|
|
# Ensure we have debootstrap intalled on the provisioner system; this is a
|
|
|
|
# good idea to include if you plan to use anything that is not part of the
|
|
|
|
# base Debian host system, just in case the provisioner host is not properly
|
|
|
|
# configured already.
|
2021-11-06 03:02:43 -04:00
|
|
|
os.system("apt-get install -y debootstrap")
|
2019-12-14 14:12:55 -05:00
|
|
|
|
|
|
|
# Perform a deboostrap installation
|
|
|
|
os.system(
|
|
|
|
"debootstrap --include={pkgs} {suite} {target} {mirror}".format(
|
|
|
|
suite=deb_release,
|
|
|
|
target=temporary_directory,
|
|
|
|
mirror=deb_mirror,
|
2021-11-06 03:02:43 -04:00
|
|
|
pkgs=",".join(deb_packages),
|
2019-12-14 14:12:55 -05:00
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
# Bind mount the devfs
|
2021-11-06 03:02:43 -04:00
|
|
|
os.system("mount --bind /dev {}/dev".format(temporary_directory))
|
2019-12-14 14:12:55 -05:00
|
|
|
|
|
|
|
# Create an fstab entry for each disk
|
|
|
|
fstab_file = "{}/etc/fstab".format(temporary_directory)
|
2020-01-05 23:41:08 -05:00
|
|
|
# The disk ID starts at zero and increments by one for each disk in the fixed-order
|
|
|
|
# disk list. This lets us work around the insanity of Libvirt IDs not matching guest IDs,
|
|
|
|
# while still letting us have some semblance of control here without enforcing things
|
|
|
|
# like labels. It increments in the for loop below at the end of each iteration, and is
|
|
|
|
# used to craft a /dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-X device ID
|
|
|
|
# which will always match the correct order from Libvirt (unlike sdX/vdX names).
|
|
|
|
disk_id = 0
|
2019-12-14 14:12:55 -05:00
|
|
|
for disk in disks:
|
|
|
|
# We assume SSD-based/-like storage, and dislike atimes
|
|
|
|
options = "defaults,discard,noatime,nodiratime"
|
|
|
|
|
2020-01-05 23:41:08 -05:00
|
|
|
# The root, var, and log volumes have specific values
|
2021-11-06 03:02:43 -04:00
|
|
|
if disk["mountpoint"] == "/":
|
|
|
|
root_disk["scsi_id"] = disk_id
|
2019-12-14 14:12:55 -05:00
|
|
|
dump = 0
|
|
|
|
cpass = 1
|
2021-11-06 03:02:43 -04:00
|
|
|
elif disk["mountpoint"] == "/var" or disk["mountpoint"] == "/var/log":
|
2019-12-14 14:12:55 -05:00
|
|
|
dump = 0
|
|
|
|
cpass = 2
|
|
|
|
else:
|
|
|
|
dump = 0
|
|
|
|
cpass = 0
|
|
|
|
|
|
|
|
# Append the fstab line
|
2021-11-06 03:02:43 -04:00
|
|
|
with open(fstab_file, "a") as fh:
|
2020-01-05 23:41:08 -05:00
|
|
|
data = "/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-{disk} {mountpoint} {filesystem} {options} {dump} {cpass}\n".format(
|
|
|
|
disk=disk_id,
|
2021-11-06 03:02:43 -04:00
|
|
|
mountpoint=disk["mountpoint"],
|
|
|
|
filesystem=disk["filesystem"],
|
2019-12-14 14:12:55 -05:00
|
|
|
options=options,
|
|
|
|
dump=dump,
|
2021-11-06 03:02:43 -04:00
|
|
|
cpass=cpass,
|
2019-12-14 14:12:55 -05:00
|
|
|
)
|
|
|
|
fh.write(data)
|
|
|
|
|
2020-01-05 23:41:08 -05:00
|
|
|
# Increment the disk_id
|
|
|
|
disk_id += 1
|
|
|
|
|
2019-12-14 14:12:55 -05:00
|
|
|
# Write the hostname
|
|
|
|
hostname_file = "{}/etc/hostname".format(temporary_directory)
|
2021-11-06 03:02:43 -04:00
|
|
|
with open(hostname_file, "w") as fh:
|
2019-12-14 14:12:55 -05:00
|
|
|
fh.write("{}".format(vm_name))
|
|
|
|
|
|
|
|
# Fix the cloud-init.target since it's broken
|
2021-11-06 03:02:43 -04:00
|
|
|
cloudinit_target_file = "{}/etc/systemd/system/cloud-init.target".format(
|
|
|
|
temporary_directory
|
|
|
|
)
|
|
|
|
with open(cloudinit_target_file, "w") as fh:
|
2019-12-14 14:12:55 -05:00
|
|
|
data = """[Install]
|
|
|
|
WantedBy=multi-user.target
|
|
|
|
[Unit]
|
|
|
|
Description=Cloud-init target
|
|
|
|
After=multi-user.target
|
|
|
|
"""
|
|
|
|
fh.write(data)
|
|
|
|
|
|
|
|
# NOTE: Due to device ordering within the Libvirt XML configuration, the first Ethernet interface
|
|
|
|
# will always be on PCI bus ID 2, hence the name "ens2".
|
|
|
|
# Write a DHCP stanza for ens2
|
|
|
|
ens2_network_file = "{}/etc/network/interfaces.d/ens2".format(temporary_directory)
|
2021-11-06 03:02:43 -04:00
|
|
|
with open(ens2_network_file, "w") as fh:
|
2019-12-14 14:12:55 -05:00
|
|
|
data = """auto ens2
|
|
|
|
iface ens2 inet dhcp
|
|
|
|
"""
|
|
|
|
fh.write(data)
|
|
|
|
|
|
|
|
# Write the DHCP config for ens2
|
|
|
|
dhclient_file = "{}/etc/dhcp/dhclient.conf".format(temporary_directory)
|
2021-11-06 03:02:43 -04:00
|
|
|
with open(dhclient_file, "w") as fh:
|
|
|
|
data = (
|
|
|
|
"""# DHCP client configuration
|
2020-01-05 22:47:10 -05:00
|
|
|
# Written by the PVC provisioner
|
2019-12-14 14:12:55 -05:00
|
|
|
option rfc3442-classless-static-routes code 121 = array of unsigned integer 8;
|
|
|
|
interface "ens2" {
|
2021-11-06 03:02:43 -04:00
|
|
|
"""
|
|
|
|
+ """ send fqdn.fqdn = "{hostname}";
|
2020-01-05 23:09:33 -05:00
|
|
|
send host-name = "{hostname}";
|
2021-11-06 03:02:43 -04:00
|
|
|
""".format(
|
|
|
|
hostname=vm_name
|
|
|
|
)
|
|
|
|
+ """ request subnet-mask, broadcast-address, time-offset, routers,
|
2019-12-14 14:12:55 -05:00
|
|
|
domain-name, domain-name-servers, domain-search, host-name,
|
|
|
|
dhcp6.name-servers, dhcp6.domain-search, dhcp6.fqdn, dhcp6.sntp-servers,
|
|
|
|
netbios-name-servers, netbios-scope, interface-mtu,
|
|
|
|
rfc3442-classless-static-routes, ntp-servers;
|
|
|
|
}
|
|
|
|
"""
|
2021-11-06 03:02:43 -04:00
|
|
|
)
|
2019-12-14 14:12:55 -05:00
|
|
|
fh.write(data)
|
|
|
|
|
|
|
|
# Write the GRUB configuration
|
|
|
|
grubcfg_file = "{}/etc/default/grub".format(temporary_directory)
|
2021-11-06 03:02:43 -04:00
|
|
|
with open(grubcfg_file, "w") as fh:
|
2019-12-14 14:12:55 -05:00
|
|
|
data = """# Written by the PVC provisioner
|
|
|
|
GRUB_DEFAULT=0
|
|
|
|
GRUB_TIMEOUT=1
|
|
|
|
GRUB_DISTRIBUTOR="PVC Virtual Machine"
|
2020-03-02 10:02:39 -05:00
|
|
|
GRUB_CMDLINE_LINUX_DEFAULT="root=/dev/disk/by-id/scsi-0QEMU_QEMU_HARDDISK_drive-scsi0-0-0-{root_disk} console=tty0 console=ttyS0,115200n8"
|
2019-12-14 14:12:55 -05:00
|
|
|
GRUB_CMDLINE_LINUX=""
|
|
|
|
GRUB_TERMINAL=console
|
|
|
|
GRUB_SERIAL_COMMAND="serial --speed=115200 --unit=0 --word=8 --parity=no --stop=1"
|
|
|
|
GRUB_DISABLE_LINUX_UUID=false
|
2021-11-06 03:02:43 -04:00
|
|
|
""".format(
|
|
|
|
root_disk=root_disk["scsi_id"]
|
|
|
|
)
|
2019-12-14 14:12:55 -05:00
|
|
|
fh.write(data)
|
|
|
|
|
|
|
|
# Chroot, do some in-root tasks, then exit the chroot
|
2021-07-11 23:10:41 -04:00
|
|
|
with chroot_target(temporary_directory):
|
|
|
|
# Install and update GRUB
|
|
|
|
os.system(
|
2021-11-06 03:02:43 -04:00
|
|
|
"grub-install --force /dev/rbd/{}/{}_{}".format(
|
|
|
|
root_disk["pool"], vm_name, root_disk["disk_id"]
|
|
|
|
)
|
2021-07-11 23:10:41 -04:00
|
|
|
)
|
2021-11-06 03:02:43 -04:00
|
|
|
os.system("update-grub")
|
2021-07-11 23:10:41 -04:00
|
|
|
# Set a really dumb root password [TEMPORARY]
|
2021-11-06 03:02:43 -04:00
|
|
|
os.system("echo root:test123 | chpasswd")
|
2021-07-11 23:10:41 -04:00
|
|
|
# Enable cloud-init target on (first) boot
|
|
|
|
# NOTE: Your user-data should handle this and disable it once done, or things get messy.
|
|
|
|
# That cloud-init won't run without this hack seems like a bug... but even the official
|
|
|
|
# Debian cloud images are affected, so who knows.
|
2021-11-06 03:02:43 -04:00
|
|
|
os.system("systemctl enable cloud-init.target")
|
2019-12-14 14:12:55 -05:00
|
|
|
|
|
|
|
# Unmount the bound devfs
|
2021-11-06 03:02:43 -04:00
|
|
|
os.system("umount {}/dev".format(temporary_directory))
|
2019-12-14 14:12:55 -05:00
|
|
|
|
|
|
|
# Everything else is done via cloud-init user-data
|