891 lines
29 KiB
Plaintext
891 lines
29 KiB
Plaintext
# Mgmt
|
|
# Copyright (C) James Shubin and the project contributors
|
|
# Written by James Shubin <james@shubin.ca> and the project contributors
|
|
#
|
|
# 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
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# 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/>.
|
|
#
|
|
# Additional permission under GNU GPL version 3 section 7
|
|
#
|
|
# If you modify this program, or any covered work, by linking or combining it
|
|
# with embedded mcl code and modules (and that the embedded mcl code and
|
|
# modules which link with this program, contain a copy of their source code in
|
|
# the authoritative form) containing parts covered by the terms of any other
|
|
# license, the licensors of this program grant you additional permission to
|
|
# convey the resulting work. Furthermore, the licensors of this program grant
|
|
# the original author, James Shubin, additional permission to update this
|
|
# additional permission if he deems it necessary to achieve the goals of this
|
|
# additional permission.
|
|
|
|
# Run `sudo setcap CAP_NET_BIND_SERVICE=+eip mgmt` first to avoid running as root.
|
|
# based on: https://docs.fedoraproject.org/en-US/fedora/f36/install-guide/advanced/Network_based_Installations/
|
|
|
|
import "convert"
|
|
import "deploy"
|
|
import "fmt"
|
|
import "golang"
|
|
import "golang/path/filepath" as golang_path_filepath
|
|
import "golang/strings" as golang_strings
|
|
import "list"
|
|
import "local"
|
|
import "net"
|
|
import "os"
|
|
import "value"
|
|
import "world"
|
|
|
|
$http_suffix = "http/"
|
|
$tftp_suffix = "tftp/"
|
|
$uefi_suffix = "uefi/"
|
|
$kickstart_suffix = "kickstart/"
|
|
|
|
# The base class is the core provisioner which can also spawn child classes.
|
|
class base($config) {
|
|
#
|
|
# variables
|
|
#
|
|
$interface = $config->interface || "eth0" # XXX: what if no interface exists?
|
|
#$interface = _struct_lookup_optional($config, "interface", "eth0")
|
|
|
|
$http_port = $config->http_port || 4280 # using :4280 avoids needing root and isn't in /etc/services
|
|
$http_port_str = fmt.printf("%d", $http_port)
|
|
|
|
$network = $config->network || "192.168.42.0/24"
|
|
$router = $config->router || "192.168.42.1/24"
|
|
$router_ip = net.cidr_to_ip($router) # removes cidr suffix
|
|
|
|
$dns = $config->dns || ["8.8.8.8", "1.1.1.1",] # maybe google/cloudflare will sponsor!
|
|
|
|
$prefix = $config->prefix || ""
|
|
panic($prefix == "") # panic if prefix is empty
|
|
panic(not golang_strings.has_suffix($prefix, "/"))
|
|
|
|
file "${prefix}" { # dir
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
$tftp_prefix = "${prefix}${tftp_suffix}"
|
|
$http_prefix = "${prefix}${http_suffix}"
|
|
$uefi_prefix = "${prefix}${uefi_suffix}"
|
|
|
|
$firewalld = $config->firewalld || true
|
|
|
|
# eg: equivalent of: https://download.fedoraproject.org/pub/fedora/linux/
|
|
$inst_repo_base = "http://${router_ip}:${http_port_str}/fedora/" # private lan online, no https!
|
|
$server_base = "http://${router_ip}:${http_port_str}/" # private lan online, no https!
|
|
|
|
$syslinux_root = "/usr/share/syslinux/"
|
|
|
|
$nbp_bios = "tftp://${router_ip}/pxelinux.0" # for bios clients
|
|
$nbp_uefi = "tftp://${router_ip}/uefi/shim.efi" # for uefi clients
|
|
|
|
#
|
|
# network
|
|
#
|
|
net "${interface}" {
|
|
state => $const.res.net.state.up,
|
|
addrs => [$router,], # has cidr suffix
|
|
#gateway => "192.168.42.1", # TODO: get upstream public gateway with new function
|
|
|
|
ip_forward => true, # XXX: does this work?
|
|
|
|
Meta:reverse => true, # XXX: ^C doesn't reverse atm. Fix that!
|
|
|
|
Before => Dhcp:Server[":67"], # TODO: add autoedges
|
|
}
|
|
|
|
#
|
|
# packages
|
|
#
|
|
# TODO: do we need "syslinux-nonlinux" ?
|
|
$pkgs_bios = ["syslinux",]
|
|
$pkgs_uefi = ["shim-x64", "grub2-efi-x64",]
|
|
pkg $pkgs_bios {
|
|
state => "installed",
|
|
}
|
|
#pkg $pkgs_uefi {
|
|
# state => "installed",
|
|
#}
|
|
|
|
# TODO: not in F39+ it seems
|
|
#$pkgs_kickstart = ["fedora-kickstarts", "spin-kickstarts",]
|
|
#pkg $pkgs_kickstart {
|
|
# state => "installed",
|
|
#}
|
|
|
|
#
|
|
# firewalld
|
|
#
|
|
if $firewalld {
|
|
firewalld "provisioner" { # name is irrelevant
|
|
services => [
|
|
"tftp",
|
|
"dhcp",
|
|
],
|
|
ports => ["${http_port_str}/tcp",],
|
|
|
|
state => $const.res.firewalld.state.exists,
|
|
}
|
|
}
|
|
|
|
file "${tftp_prefix}" { # dir
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
file "${uefi_prefix}" { # dir
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
|
|
#
|
|
# tftp
|
|
#
|
|
tftp:server ":69" {
|
|
timeout => 60, # increase the timeout
|
|
#root => $root, # we're running in memory without needing a root!
|
|
#debug => true, # XXX: print out a `tree` representation in tmp prefix for the user
|
|
|
|
Depend => Pkg[$pkgs_bios], # hosted by tftp
|
|
#Depend => Pkg[$pkgs_uefi],
|
|
}
|
|
|
|
#
|
|
# bios bootloader images
|
|
#
|
|
|
|
# XXX: should this also be part of repo too?
|
|
class tftp_root_file($f) {
|
|
#tftp:file "${f}" { # without root slash
|
|
tftp:file "/${f}" { # with root slash
|
|
path => $syslinux_root + $f, # TODO: add autoedges
|
|
|
|
Depend => Pkg[$pkgs_bios],
|
|
}
|
|
}
|
|
include tftp_root_file("pxelinux.0")
|
|
include tftp_root_file("vesamenu.c32")
|
|
include tftp_root_file("ldlinux.c32")
|
|
include tftp_root_file("libcom32.c32")
|
|
include tftp_root_file("libutil.c32")
|
|
|
|
#
|
|
# dhcp
|
|
#
|
|
dhcp:server ":67" {
|
|
interface => $interface, # required for now
|
|
leasetime => "10m", # seems some clients refresh half way at 5m
|
|
dns => $dns, # pick your own better ones!
|
|
routers => [$router_ip,],
|
|
|
|
serverid => $router_ip, # XXX: test automatic mode
|
|
|
|
#Depend => Net[$interface], # TODO: add autoedges
|
|
}
|
|
|
|
#
|
|
# http
|
|
#
|
|
file "${http_prefix}" { # dir
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
|
|
http:server ":${http_port_str}" {
|
|
#address => ":${http_port_str}", # you can override the name like this
|
|
#timeout => 60, # add a timeout (seconds)
|
|
}
|
|
|
|
$kickstart_http_prefix = "${http_prefix}${kickstart_suffix}"
|
|
file "${kickstart_http_prefix}" {
|
|
state => $const.res.file.state.exists,
|
|
#source => "", # this default means empty directory
|
|
recurse => true,
|
|
purge => true, # remove unmanaged files in here
|
|
}
|
|
|
|
$vardir = local.vardir("provisioner/")
|
|
$binary_path = deploy.binary_path()
|
|
|
|
http:file "/mgmt/binary" { # TODO: support different architectures
|
|
path => $binary_path, # TODO: As long as binary doesn't contain private data!
|
|
|
|
Before => Print["ready"],
|
|
}
|
|
|
|
# XXX: don't put anything private in your code this isn't authenticated!
|
|
$abs_tar = "${vardir}deploys/deploy.tar"
|
|
$abs_gz = "${abs_tar}.gz"
|
|
|
|
file "${vardir}deploys/" {
|
|
state => $const.res.file.state.exists,
|
|
recurse => true,
|
|
purge => true,
|
|
owner => "root",
|
|
group => "root",
|
|
mode => "u=rwx,g=rx,o=", # dir
|
|
}
|
|
# Tag this so that the folder purge doesn't remove it. (XXX: bug!)
|
|
file "${abs_tar}" {
|
|
owner => "root",
|
|
group => "root",
|
|
mode => "u=rw,g=rw,o=", # file
|
|
|
|
Meta:retry => -1, # changing the mode on this file can be racy
|
|
}
|
|
file "${abs_gz}" {
|
|
owner => "root",
|
|
group => "root",
|
|
mode => "u=rw,g=rw,o=", # file
|
|
|
|
Meta:retry => -1, # changing the mode on this file can be racy
|
|
}
|
|
deploy:tar "${abs_tar}" {
|
|
Before => Gzip["${abs_gz}"],
|
|
Depend => File["${vardir}deploys/"], # make the dir first!
|
|
}
|
|
gzip "${abs_gz}" {
|
|
input => "${abs_tar}",
|
|
}
|
|
http:file "/mgmt/deploy.tar.gz" {
|
|
path => "${abs_gz}",
|
|
}
|
|
|
|
print "ready" {
|
|
msg => "ready to provision!",
|
|
|
|
Meta:autogroup => false,
|
|
|
|
Depend => Tftp:Server[":69"],
|
|
Depend => Dhcp:Server[":67"],
|
|
Depend => Http:Server[":${http_port_str}"],
|
|
}
|
|
|
|
# we're effectively returning a new class definition...
|
|
}
|
|
|
|
# The repo class which is a child of base, defines the distro repo to use.
|
|
class base:repo($config) {
|
|
|
|
$distro = $config->distro || "fedora"
|
|
$version = $config->version || "39" # not an int!
|
|
$arch = $config->arch || "x86_64"
|
|
#$flavour = $config->flavour || "" # is flavour needed for repo sync?
|
|
|
|
# export this value to parent scope for base:host to consume
|
|
$uid = "${distro}${version}-${arch}" # eg: fedora39-x86_64
|
|
|
|
# TODO: We need a way to pick a good default because if a lot of people
|
|
# use this, then most won't change it to one in their country...
|
|
$mirror = $config->mirror || "" # TODO: how do we pick a default?
|
|
$rsync = $config->rsync || ""
|
|
|
|
$is_fedora = $distro == "fedora"
|
|
|
|
$distroarch_tftp_prefix = "${tftp_prefix}${uid}/"
|
|
$distroarch_uefi_prefix = "${uefi_prefix}${uid}/"
|
|
$distroarch_http_prefix = "${http_prefix}${uid}/"
|
|
$distroarch_release_http_prefix = "${distroarch_http_prefix}release/"
|
|
$distroarch_updates_http_prefix = "${distroarch_http_prefix}updates/"
|
|
|
|
file "${distroarch_tftp_prefix}" { # dir
|
|
state => $const.res.file.state.exists,
|
|
|
|
#Meta:quiet => true, # TODO
|
|
}
|
|
file "${distroarch_uefi_prefix}" { # dir
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
file "${distroarch_http_prefix}" { # root http dir
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
file "${distroarch_release_http_prefix}" {
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
file "${distroarch_updates_http_prefix}" {
|
|
state => $const.res.file.state.exists,
|
|
}
|
|
|
|
#
|
|
# uefi bootloader images
|
|
#
|
|
$uefi_download_dir = "${distroarch_uefi_prefix}download/"
|
|
$uefi_extract_dir = "${distroarch_uefi_prefix}extract/"
|
|
|
|
file "${uefi_extract_dir}" { # mkdir
|
|
state => $const.res.file.state.exists,
|
|
|
|
Depend => Exec["uefi-download-${uid}"],
|
|
Before => Exec["uefi-extract-${uid}"],
|
|
}
|
|
|
|
# Download the shim and grub2-efi packages. If your server is a BIOS
|
|
# system, you must download the packages to a temporary install root.
|
|
# Installing them directly on a BIOS machine will attempt to configure
|
|
# the system for UEFI booting and cause problems.
|
|
$pkgs_uefi_string = golang_strings.join($pkgs_uefi, " ")
|
|
$repoidname = "local"
|
|
# eg: https://mirror.csclub.uwaterloo.ca/fedora/linux/releases/39/Everything/x86_64/os/
|
|
$repo_url = "http://${router_ip}:${http_port_str}/fedora/releases/${version}/Everything/${arch}/os/"
|
|
|
|
exec "uefi-download-${uid}" {
|
|
# no inner quotes because it's not bash handling this!
|
|
# the dnf download command makes the download destination dir
|
|
# With DNF5 (provisioning OS Fedora 41+) it's --destdir.
|
|
# Previously it's --downloaddir and no idea why the API break.
|
|
cmd => "/usr/bin/dnf download ${pkgs_uefi_string} --assumeyes --disablerepo=* --repofrompath ${repoidname},${repo_url} --destdir=${uefi_download_dir} --releasever ${version}",
|
|
|
|
# TODO: add an optional expiry mtime check that deletes these old files with an || rm * && false
|
|
ifcmd => "! test -s '${uefi_download_dir}shim-x64'*",
|
|
ifshell => "/usr/bin/bash",
|
|
|
|
Depend => Http:Server[":${http_port_str}"],
|
|
}
|
|
|
|
exec "uefi-extract-${uid}" {
|
|
# we use rpm2archive instead of cpio since the latter is deprecated for big files
|
|
# we do this in a loop for all the rpm files
|
|
cmd => "for i in ${uefi_download_dir}*.rpm; do /usr/bin/rpm2archive \$i | /usr/bin/tar -xvz --directory ${uefi_extract_dir} --exclude ./etc; done",
|
|
shell => "/usr/bin/bash",
|
|
|
|
# TODO: add an optional expiry mtime check that deletes these old files with an || rm * && false
|
|
creates => $uefi_shim,
|
|
|
|
Depend => Exec["uefi-download-${uid}"],
|
|
Before => Tftp:Server[":69"],
|
|
}
|
|
|
|
$uefi_root = "${uefi_extract_dir}/boot/efi/EFI/fedora/"
|
|
$uefi_shim = "${uefi_root}shim.efi"
|
|
$uefi_shimx64 = "${uefi_root}shimx64.efi"
|
|
tftp:file "/uefi/shim.efi" { # needs leading slash
|
|
path => $uefi_shim, # TODO: add autoedges
|
|
|
|
Depend => Exec["uefi-extract-${uid}"],
|
|
}
|
|
tftp:file "/uefi/shimx64.efi" { # needs leading slash
|
|
path => $uefi_shimx64, # TODO: add autoedges
|
|
|
|
Depend => Exec["uefi-extract-${uid}"],
|
|
}
|
|
tftp:file "/uefi/grubx64.efi" { # sometimes used?
|
|
path => "${uefi_root}grubx64.efi", # TODO: add autoedges
|
|
|
|
Depend => Exec["uefi-extract-${uid}"],
|
|
}
|
|
tftp:file "grubx64.efi" { # no leading slash
|
|
path => "${uefi_root}grubx64.efi", # TODO: add autoedges
|
|
|
|
Depend => Exec["uefi-extract-${uid}"],
|
|
}
|
|
|
|
# XXX: replace with a download resource
|
|
# XXX: allow send->recv to pass this file to tftp:file->data to keep it in mem!
|
|
$vmlinuz_file = "${distroarch_tftp_prefix}vmlinuz"
|
|
exec "vmlinuz-${uid}" {
|
|
cmd => "/usr/bin/wget",
|
|
args => [
|
|
"--no-verbose",
|
|
"${repo_url}images/pxeboot/vmlinuz",
|
|
"-O",
|
|
$vmlinuz_file,
|
|
],
|
|
creates => $vmlinuz_file,
|
|
|
|
Depend => File[$distroarch_tftp_prefix],
|
|
Depend => Http:Server[":${http_port_str}"],
|
|
Before => Print["ready"],
|
|
}
|
|
|
|
tftp:file "/${uid}/vmlinuz" {
|
|
path => $vmlinuz_file, # TODO: add autoedges
|
|
|
|
#Depend => Pkg[$pkgs],
|
|
}
|
|
http:file "/${uid}/vmlinuz" { # when using ipxe
|
|
path => $vmlinuz_file, # TODO: add autoedges
|
|
|
|
#Depend => Pkg[$pkgs],
|
|
}
|
|
|
|
$initrd_file = "${distroarch_tftp_prefix}initrd.img"
|
|
exec "initrd-${uid}" {
|
|
cmd => "/usr/bin/wget",
|
|
args => [
|
|
"--no-verbose",
|
|
"${repo_url}images/pxeboot/initrd.img",
|
|
"-O",
|
|
$initrd_file,
|
|
],
|
|
creates => $initrd_file,
|
|
|
|
Depend => File[$distroarch_tftp_prefix],
|
|
Depend => Http:Server[":${http_port_str}"],
|
|
Before => Print["ready"],
|
|
}
|
|
|
|
tftp:file "/${uid}/initrd.img" {
|
|
path => $initrd_file, # TODO: add autoedges
|
|
|
|
#Depend => Pkg[$pkgs],
|
|
}
|
|
http:file "/${uid}/initrd.img" { # when using ipxe
|
|
path => $initrd_file, # TODO: add autoedges
|
|
|
|
#Depend => Pkg[$pkgs],
|
|
}
|
|
|
|
# this file resource serves the entire rsync directory over http
|
|
if $mirror == "" { # and $rsync != ""
|
|
http:file "/fedora/releases/${version}/Everything/${arch}/os/" {
|
|
path => $distroarch_release_http_prefix,
|
|
}
|
|
http:file "/fedora/updates/${version}/Everything/${arch}/" {
|
|
path => $distroarch_updates_http_prefix,
|
|
}
|
|
} else {
|
|
# same as the above http:file path would have been
|
|
http:proxy "/fedora/releases/${version}/Everything/${arch}/os/" {
|
|
sub => "/fedora/", # we remove this from the name!
|
|
head => $mirror,
|
|
|
|
cache => $distroarch_release_http_prefix, # $prefix/http/fedora39-x86_64/release/
|
|
}
|
|
|
|
# XXX: if we had both of these in the same http_prefix, we could overlap them with an rsync :/ hmm...
|
|
http:proxy "/fedora/updates/${version}/Everything/${arch}/" { # no os/ dir at the end
|
|
sub => "/fedora/", # we remove this from the name!
|
|
head => $mirror,
|
|
|
|
cache => $distroarch_updates_http_prefix, # $prefix/http/fedora39-x86_64/updates/
|
|
}
|
|
}
|
|
|
|
# .repo files
|
|
#$fedora_repo_template = struct{
|
|
# name => "fedora",
|
|
# distro => $distro,
|
|
# version => $version, # 39 for fedora 39
|
|
# arch => $arch, # could also be aarch64
|
|
# #flavour => "Everything", # The install repo uses "Everything" even for "Workstation" or "Server"
|
|
# extra => "releases",
|
|
#
|
|
# baseurl => "http://${router_ip}:${http_port_str}/fedora/releases/${version}/Everything/${arch}/os/",
|
|
#}
|
|
#$updates_repo_template = struct{
|
|
# name => "updates",
|
|
# distro => $distro,
|
|
# version => $version, # 39 for fedora 39
|
|
# arch => $arch, # could also be aarch64
|
|
# #flavour => "Everything", # The install repo uses "Everything" even for "Workstation" or "Server"
|
|
# extra => "updates",
|
|
#
|
|
# baseurl => "http://${router_ip}:${http_port_str}/fedora/updates/${version}/Everything/${arch}/",
|
|
#}
|
|
#http:file "/fedora/${uid}/fedora.repo" {
|
|
# data => golang.template(deploy.readfile("/files/repo.tmpl"), $fedora_repo_template),
|
|
#}
|
|
#http:file "/fedora/${uid}/updates.repo" {
|
|
# data => golang.template(deploy.readfile("/files/repo.tmpl"), $updates_repo_template),
|
|
#}
|
|
|
|
#
|
|
# rsync
|
|
#
|
|
#$source_pattern = if $is_fedora {
|
|
# "${rsync}releases/${version}/Everything/${arch}/os/" # source
|
|
#} else {
|
|
# "" # XXX: not implemented
|
|
#}
|
|
#panic($source_pattern == "") # distro is not specified
|
|
# TODO: combine release and updates?
|
|
#$is_safe = $distroarch_release_http_prefix != "" and $distroarch_release_http_prefix != "/"
|
|
#if $rsync != "" and $source_pattern != "" and $is_safe {
|
|
#
|
|
# $mtime_file = "${http_prefix}rsync-${uid}.mtime"
|
|
# $delta = convert.int_to_str(60 * 60 * 24 * 7) # ~1 week in seconds: 604800
|
|
# exec "rsync-${uid}" {
|
|
# cmd => "/usr/bin/rsync",
|
|
# args => [
|
|
# "-avSH",
|
|
# "--progress",
|
|
# # This flavour must always be Everything to work.
|
|
# # The Workstation flavour doesn't have an os/ dir.
|
|
# $source_pattern, # source
|
|
# $distroarch_release_http_prefix, # dest
|
|
# ],
|
|
#
|
|
# # run this when cmd completes successfully
|
|
# donecmd => "/usr/bin/date --utc > ${mtime_file}",
|
|
# doneshell => "/usr/bin/bash",
|
|
#
|
|
# # Run if the difference between the current date and the
|
|
# # saved date (both converted to sec) is greater than the
|
|
# # delta! (Or if the mtime file does not even exist yet.)
|
|
# ifcmd => "! /usr/bin/test -e ${mtime_file} || /usr/bin/test \$((`/usr/bin/date +%s` - `/usr/bin/stat -c %Y '${mtime_file}'`)) -gt ${delta}",
|
|
#
|
|
# ifshell => "/usr/bin/bash",
|
|
#
|
|
# Before => Http:Server[":${http_port_str}"],
|
|
# Before => File[$distroarch_release_http_prefix],
|
|
# }
|
|
#}
|
|
}
|
|
|
|
# The host class is used for each physical host we want to provision.
|
|
class base:host($name, $config) {
|
|
#print $name {
|
|
# msg => "host: ${name}",
|
|
#
|
|
# Meta:autogroup => false,
|
|
#}
|
|
$repouid = $config->repo || ""
|
|
$uidst = os.parse_distro_uid($repouid)
|
|
$distro = $uidst->distro
|
|
$version = $uidst->version # not an int!
|
|
$arch = $uidst->arch
|
|
panic($distro == "")
|
|
panic($version == "")
|
|
panic($arch == "")
|
|
$flavour = $config->flavour || ""
|
|
|
|
$mac = $config->mac || ""
|
|
#panic($mac == "") # provision anyone by default
|
|
$ip = $config->ip || "" # XXX: auto-generate it inside of the above network somehow (see below)
|
|
panic($ip == "")
|
|
#$ns = if $config->ip == "" {
|
|
# ""
|
|
#} else {
|
|
# "" + get_value("network") # XXX: implement some sort of lookup function
|
|
#}
|
|
#$ip = $config->ip || magic.pool($ns, [1,2,3,4], $name) # XXX: if $ns is "", then don't allocate. Otherwise get from list. Reuse based on $name hash.
|
|
$bios = $config->bios || false
|
|
$password = $config->password || "" # empty means disabled
|
|
panic(len($password) != 0 and len($password) != 106) # length of salted password
|
|
|
|
$part = $config->part || "" # partitioning scheme
|
|
|
|
$empty_list_str []str = [] # need an explicit type on empty list definition
|
|
$packages = $config->packages || $empty_list_str
|
|
|
|
# should we provision this host by default?
|
|
$provision_default = $config->provision || false # false is safest!
|
|
|
|
$handoff_type = $config->handoff || ""
|
|
$handoff_code = $config->handoff_code || ""
|
|
$handoff_module_path = $config->handoff_module_path || ""
|
|
panic($handoff_code != "" and not golang_strings.has_prefix($handoff_code, "/"))
|
|
$handoff_hostname = $config->handoff_hostname || ""
|
|
|
|
# This is a giant driver://user:password@host:port/whatever URL...
|
|
$bmc_uri = $config->bmc_uri || ""
|
|
|
|
# unique host key which is usually a mac address unless it's a default
|
|
$hkey = if $mac == "" {
|
|
"default"
|
|
} else {
|
|
$mac
|
|
}
|
|
$provision_key = $hkey # XXX: what unique id should we use for the host? mac? name? hkey?
|
|
|
|
#$ret = world.getval($provision_key) # has it previously been provisioned?
|
|
#$val = if $ret->value == "" { # avoid an invalid string killing the parse_bool function
|
|
# convert.format_bool(false) # "false"
|
|
#} else {
|
|
# $ret->value
|
|
#}
|
|
#$provision = if not $ret->exists {
|
|
# $provision_default
|
|
#} else {
|
|
# not convert.parse_bool($val) # XXX: should an invalid string return false or error here?
|
|
#}
|
|
$provision = true
|
|
|
|
$nbp_path = if $bios {
|
|
"/pxelinux.0" # for bios clients
|
|
} else {
|
|
"/uefi/shim.efi" # for uefi clients
|
|
}
|
|
|
|
if $mac != "" {
|
|
dhcp:host "${name}" { # the hostname
|
|
mac => $mac,
|
|
ip => $ip, # cidr notation is required
|
|
|
|
nbp => $provision ?: if $bios { # XXX: do we want this from the base class?
|
|
$nbp_bios # from base class
|
|
} else {
|
|
$nbp_uefi # from base class
|
|
},
|
|
nbp_path => $provision ?: $nbp_path, # with leading slash
|
|
|
|
Depend => Tftp:Server[":69"],
|
|
}
|
|
} else {
|
|
# Handle ANY mac address since we don't have one specified!
|
|
# TODO: Our dhcp:range could send/recv a map from ip => mac address!
|
|
dhcp:range "${name}" {
|
|
network => "${network}", # eg: 192.168.42.0/24
|
|
skip => [$router,], # eg: 192.168.42.1/24
|
|
|
|
nbp => $provision ?: if $bios { # XXX: do we want this from the base class?
|
|
$nbp_bios # from base class
|
|
} else {
|
|
$nbp_uefi # from base class
|
|
},
|
|
nbp_path => $provision ?: $nbp_path, # with leading slash
|
|
|
|
Depend => Tftp:Server[":69"],
|
|
}
|
|
}
|
|
|
|
$menu_template = struct{
|
|
distro => $distro,
|
|
version => $version, # 39 for fedora 39
|
|
arch => $arch, # could also be aarch64
|
|
flavour => "Everything", # The install repo uses "Everything" even for "Workstation" or "Server"
|
|
|
|
ks => "http://${router_ip}:${http_port_str}/fedora/kickstart/${hkey}.ks", # usually $mac or `default`
|
|
inst_repo_base => $inst_repo_base,
|
|
server_base => $server_base,
|
|
}
|
|
|
|
#
|
|
# default menus
|
|
#
|
|
$safe_mac = if $mac == "" {
|
|
"00:00:00:00:00:00"
|
|
} else {
|
|
$mac
|
|
}
|
|
$old_mac = net.oldmacfmt($safe_mac)
|
|
# no idea why these need a 01- prefix
|
|
$bios_menu = if $mac == "" {
|
|
"/pxelinux.cfg/default"
|
|
} else {
|
|
# /pxelinux.cfg/01-00-11-22-33-44-55-66
|
|
"/pxelinux.cfg/01-${old_mac}"
|
|
}
|
|
$uefi_menu = if $mac == "" {
|
|
# XXX: add the front slash!?
|
|
#"pxelinux/uefi" # TODO: Did some machines use this?
|
|
"/uefi/grub.cfg"
|
|
} else {
|
|
# /uefi/grub.cfg-01-00-11-22-33-44-55-66
|
|
"/uefi/grub.cfg-01-${old_mac}"
|
|
}
|
|
|
|
$ipxe_menu = if $mac == "" {
|
|
"menu.ipxe"
|
|
} else {
|
|
"${old_mac}.ipxe"
|
|
}
|
|
|
|
if $bios {
|
|
tftp:file "${bios_menu}" { # for bios
|
|
data => golang.template(deploy.readfile("/files/bios-menu.tmpl"), $menu_template),
|
|
}
|
|
} else {
|
|
tftp:file "${uefi_menu}" { # for uefi
|
|
# XXX: linuxefi & initrdefi VS. kernel & append ?
|
|
data => golang.template(deploy.readfile("/files/uefi-menu.tmpl"), $menu_template),
|
|
|
|
#Depend => Pkg[$pkgs_uefi],
|
|
#Depend => Exec["uefi-extract"],
|
|
}
|
|
}
|
|
|
|
http:file "/${ipxe_menu}" { # for ipxe
|
|
data => golang.template(deploy.readfile("/files/ipxe-menu.tmpl"), $menu_template),
|
|
}
|
|
|
|
# If it's a dir we don't need a suffix, otherwise return the last chunk.
|
|
$handoff_code_chunk = if golang_strings.has_suffix($prefix, "/") {
|
|
""
|
|
} else {
|
|
golang_path_filepath.base($handoff_code)
|
|
}
|
|
if $handoff_code != "" { # it's a file path or dir!
|
|
$abs_tar = "${vardir}deploys/deploy-${provision_key}.tar"
|
|
$abs_gz = "${abs_tar}.gz"
|
|
|
|
# Tag this so that the folder purge doesn't remove it. (XXX: bug!)
|
|
file "${abs_tar}" {
|
|
owner => "root",
|
|
group => "root",
|
|
mode => "u=rw,g=rw,o=", # file
|
|
|
|
Meta:retry => -1, # changing the mode on this file can be racy
|
|
}
|
|
file "${abs_gz}" {
|
|
owner => "root",
|
|
group => "root",
|
|
mode => "u=rw,g=rw,o=", # file
|
|
|
|
Meta:retry => -1, # changing the mode on this file can be racy
|
|
}
|
|
tar "${abs_tar}" {
|
|
inputs => [
|
|
$handoff_code, # code comes in here!
|
|
golang_strings.trim_suffix($handoff_module_path, "/"), # remove trailing slash to include the dir name
|
|
],
|
|
|
|
Before => Gzip["${abs_gz}"],
|
|
Depend => File["${vardir}deploys/"], # make the dir first!
|
|
}
|
|
gzip "${abs_gz}" {
|
|
input => "${abs_tar}",
|
|
}
|
|
http:file "/mgmt/deploy-${provision_key}.tar.gz" {
|
|
path => "${abs_gz}",
|
|
}
|
|
}
|
|
|
|
$handoff_binary_path = "/usr/local/bin/mgmt" # we install it here
|
|
$firstboot_scripts_dir = "/var/lib/mgmt-firstboot/" # TODO: /usr/lib/ instead?
|
|
$firstboot_done_dir = "/var/lib/mgmt-firstboot/done/"
|
|
$deploy_dir = "/root/mgmt-deploy/" # deploy code dir
|
|
$modules_chunk = golang_path_filepath.base(golang_strings.trim_suffix($handoff_module_path, "/")) # "modules" typically
|
|
$deploy_dir_modules = "${deploy_dir}${modules_chunk}/"
|
|
# TODO: we can customize these more precisely based on $handoff_type
|
|
$handoff_packages = deploy.bootstrap_packages($distro) # TODO: catch errors here with || []
|
|
panic($handoff_type != "" and len($handoff_packages) == 0)
|
|
#$handoff_packages_string = golang_strings.join($handoff_packages, " ")
|
|
|
|
$handoff_binary = if $handoff_type == "" {
|
|
""
|
|
} else {
|
|
# Copy over the actual mgmt binary. This enables a lot below...
|
|
"/usr/bin/wget -O '${handoff_binary_path}' 'http://${router_ip}:${http_port_str}/mgmt/binary' && /usr/bin/chmod u+x '${handoff_binary_path}'"
|
|
}
|
|
$handoff_cpcode = if $handoff_type == "" {
|
|
""
|
|
} else {
|
|
# Download a tar ball of our code.
|
|
# TODO: Alternate mechanisms of getting the code are possible.
|
|
if $handoff_code != "" {
|
|
"/usr/bin/wget -O /root/mgmt-deploy.tar.gz 'http://${router_ip}:${http_port_str}/mgmt/deploy-${provision_key}.tar.gz' && /usr/bin/mkdir '${deploy_dir}' && /usr/bin/tar -xf /root/mgmt-deploy.tar.gz --directory '${deploy_dir}'"
|
|
} else {
|
|
"/usr/bin/wget -O /root/mgmt-deploy.tar.gz 'http://${router_ip}:${http_port_str}/mgmt/deploy.tar.gz' && /usr/bin/mkdir '${deploy_dir}' && /usr/bin/tar -xf /root/mgmt-deploy.tar.gz --directory '${deploy_dir}'"
|
|
}
|
|
}
|
|
$handoff_service = if $handoff_type == "" {
|
|
""
|
|
} else {
|
|
# Setup the mgmt service, which starts on firstboot.
|
|
"${handoff_binary_path} setup svc --binary-path='${handoff_binary_path}' --install --enable"
|
|
}
|
|
$handoff_firstboot = if $handoff_type == "" {
|
|
""
|
|
} else {
|
|
# Setup the firstboot service itself.
|
|
"${handoff_binary_path} setup firstboot --binary-path='${handoff_binary_path}' --mkdir --install --enable --scripts-dir='${firstboot_scripts_dir}' --done-dir='${firstboot_done_dir}'"
|
|
}
|
|
$handoff_deploy = if $handoff_type == "" {
|
|
""
|
|
} else {
|
|
# Add a script that will run by our firstboot service on boot.
|
|
# It seems that the deploy will hang until mgmt is started...
|
|
# NOTE: We need to add the $handoff_code arg the same way it was
|
|
# passed into the provisioner. It's just now in a deploy subdir.
|
|
# If it's a dir, then this becomes the empty strings.
|
|
# XXX: The deploy could instead happen over the network to etcd.
|
|
"echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-deploy.sh && echo '${handoff_binary_path} deploy lang --seeds=http://127.0.0.1:2379 --no-git --module-path=${deploy_dir_modules} ${deploy_dir}${handoff_code_chunk}' >> ${firstboot_scripts_dir}mgmt-deploy.sh && chmod u+x ${firstboot_scripts_dir}mgmt-deploy.sh"
|
|
}
|
|
|
|
# TODO: Do we want to signal an http:flag if we're a "default" host?
|
|
$provisioning_done = if $provision_key == "default" {
|
|
""
|
|
} else {
|
|
"/usr/bin/wget --post-data 'done=true&password=sha1TODO' -O - 'http://${router_ip}:${http_port_str}/action/done/mac=${provision_key}'"
|
|
}
|
|
|
|
$http_kickstart_template = struct{
|
|
comment => "hello!",
|
|
lang => [
|
|
"en_CA.UTF-8",
|
|
"fr_CA.UTF-8",
|
|
"en_US.UTF-8",
|
|
],
|
|
password => $password, # salted
|
|
bios => $bios,
|
|
part => $part,
|
|
flavour => $flavour,
|
|
url => "http://${router_ip}:${http_port_str}/fedora/releases/${version}/Everything/${arch}/os/",
|
|
repos => {
|
|
#"fedora" => "http://${router_ip}:${http_port_str}/fedora/releases/${version}/Everything/${arch}/os/", # TODO: this vs url ?
|
|
"updates" => "http://${router_ip}:${http_port_str}/fedora/updates/${version}/Everything/${arch}/",
|
|
},
|
|
#repos => { # needs internet or blocks at storage https://bugzilla.redhat.com/show_bug.cgi?id=2269752
|
|
# "fedora" => "https://mirrors.fedoraproject.org/mirrorlist?repo=fedora-\$releasever&arch=\$basearch",
|
|
# "updates" => "https://mirrors.fedoraproject.org/mirrorlist?repo=updates-released-f\$releasever&arch=\$basearch",
|
|
#},
|
|
# We need $handoff_packages installed in the _KICKSTART_ environ
|
|
# so that we can actually run mgmt to do our work for us below!
|
|
packages => list.concat($packages, $handoff_packages),
|
|
# XXX: is wget segfaulting in the kickstart environment? curl is OK.
|
|
#pre => [
|
|
# "/usr/bin/mkdir /etc/yum.repos.d/",
|
|
# "/usr/bin/wget 'http://${router_ip}:${http_port_str}/fedora/${repouid}/fedora.repo' -O /etc/yum.repos.d/fedora.repo",
|
|
# "/usr/bin/wget 'http://${router_ip}:${http_port_str}/fedora/${repouid}/updates.repo' -O /etc/yum.repos.d/updates.repo",
|
|
# "/usr/bin/dnf --enablerepo=fedora,updates install --assumeyes ${handoff_packages_string}",
|
|
#],
|
|
hostname => $handoff_hostname,
|
|
post => [
|
|
$handoff_binary, # copy over the binary
|
|
$handoff_cpcode, # copy over a bundle of code
|
|
$handoff_service, # install a service for mgmt
|
|
$handoff_firstboot, # install firstboot service
|
|
$handoff_deploy, # install a firstboot script to deploy
|
|
$provisioning_done, # send a done signal back here
|
|
],
|
|
}
|
|
|
|
$kickstart_file = "${kickstart_http_prefix}${hkey}.ks"
|
|
file "${kickstart_file}" {
|
|
state => $const.res.file.state.exists,
|
|
content => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template),
|
|
}
|
|
|
|
http:file "/fedora/kickstart/${hkey}.ks" { # usually $mac or `default`
|
|
#data => golang.template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template),
|
|
path => $kickstart_file,
|
|
|
|
Before => Print["ready"],
|
|
}
|
|
|
|
if "${bmc_uri}" != "" {
|
|
bmc:power "${bmc_uri}" { # TODO: Name() API is not yet stable
|
|
#password => "hunter2",
|
|
insecure_password => true, # XXX: get from uri for now
|
|
|
|
state => "on",
|
|
|
|
Meta:poll => 60, # required until BMC's support real events!
|
|
}
|
|
}
|
|
|
|
##$str_true = convert.format_bool(true)
|
|
##$str_false = convert.format_bool(false)
|
|
#http:flag "${name}" {
|
|
# key => "done",
|
|
# path => "/action/done/mac=${provision_key}",
|
|
# #mapped => {$str_true => $str_true, $str_false => $str_false,},
|
|
#}
|
|
#kv "${name}" {
|
|
# key => $provision_key,
|
|
#}
|
|
#value "${provision_key}" {
|
|
# #any => true, # bool
|
|
#}
|
|
#Http:Flag["${name}"].value -> Kv["${name}"].value
|
|
#Http:Flag["${name}"].value -> Value["${provision_key}"].any
|
|
##$st_provisioned = value.get_bool($provision_key)
|
|
#$st_provisioned = value.get_str($provision_key)
|
|
#$provisioned = $st_provisioned->ready and $st_provisioned->value == "true" # export this value to parent scope
|
|
}
|