# 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:server: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:server: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:server: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:server: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:server:file "/fedora/releases/${version}/Everything/${arch}/os/" {
			path => $distroarch_release_http_prefix,
		}
		http:server:file "/fedora/updates/${version}/Everything/${arch}/" {
			path => $distroarch_updates_http_prefix,
		}
	} else {
		# same as the above http:server:file path would have been
		http:server: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:server: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:server:file "/fedora/${uid}/fedora.repo" {
	#	data => golang.template(deploy.readfile("/files/repo.tmpl"), $fedora_repo_template),
	#}
	#http:server: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
	$luks = $config->luks || true	# enable luks encryption

	$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_exec = $config->handoff_exec || ""
	$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:server: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:server: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, " ")

	$sshkey_flag = "sshkey"
	$sshkey_type = "ed25519" # TODO: support other options
	$sshkey_path = "/root/.ssh/id_${sshkey_type}"
	$setup_sshkey = "mkdir -p '/root/.ssh/' -m 700 && ssh-keygen -N '' -t '${sshkey_type}' -f '${sshkey_path}'" # non-interactive
	# Setting up the known_hosts file isn't needed since that can be done on
	# the first run of mgmt, and the initial key can come in from --ssh-url.

	$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 == "code" {
		# Download a tar ball of our code.
		# TODO: Alternate mechanisms of getting the code are possible.
		if $mac != "" {
			"/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}'"
		}
	} else {
		""
	}
	$handoff_service = if $handoff_type == "code" { # TODO: maybe add it in other scenarios
		# Setup the mgmt service, which starts on firstboot.
		"${handoff_binary_path} setup svc --binary-path='${handoff_binary_path}' --install --enable"
	} else {
		""
	}
	$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_firstboot_exec = if $handoff_type == "exec" and $handoff_exec != "" {
		# Add a script that will run by our firstboot service on boot.
		# This usually just runs mgmt over the network to etcd.
		"echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-exec.sh && echo 'ulimit -n 16384' >> ${firstboot_scripts_dir}mgmt-exec.sh && echo '${handoff_exec}' >> ${firstboot_scripts_dir}mgmt-exec.sh && chmod u+x ${firstboot_scripts_dir}mgmt-exec.sh"
	} else {
		""
	}

	$handoff_firstboot_code = if $handoff_type == "code" {
		# 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.
		"echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-code.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-code.sh && chmod u+x ${firstboot_scripts_dir}mgmt-code.sh"
	} else {
		""
	}

	# TODO: Do we want to signal an http:server:flag if we're a "default" host?
	$provisioning_done = "/usr/bin/curl --data-urlencode 'done=true' --data-urlencode 'password=sha1TODO' --data-urlencode 'sshtype=ssh-${sshkey_type}' --data-urlencode \"${sshkey_flag}=\$(cut -d ' ' -f 2 '${sshkey_path}.pub')\" --data-urlencode 'sshcomment=root@${handoff_hostname}' -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,
		luks => true, # set empty password
		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 => [
			$setup_sshkey, # setup the ssh key
			$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_firstboot_exec, # install a firstboot script to exec
			$handoff_firstboot_code, # 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:server: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:server:flag "${name}-done" {
		key => "done",
		path => "/action/done/mac=${provision_key}",
		#mapped => {$str_true => $str_true, $str_false => $str_false,},
	}
	http:server:flag "${name}-sshkey" {
		key => "${sshkey_flag}",
		path => "/action/done/mac=${provision_key}",
	}

	# TODO: rename the names of kv and value here?
	kv "${name}-done" {
		key => $provision_key,
	}
	value "${name}-done" {
		#any => true,	# bool
	}
	kv "${name}-sshkey" {
		key => $provision_key,
	}
	value "${name}-sshkey" {
		#any => "",
	}

	Http:Server:Flag["${name}-done"].value -> Kv["${name}-done"].value
	Http:Server:Flag["${name}-done"].value -> Value["${name}-done"].any
	Http:Server:Flag["${name}-sshkey"].value -> Kv["${name}-sshkey"].value
	Http:Server:Flag["${name}-sshkey"].value -> Value["${name}-sshkey"].any

	#$st_provisioned = value.get_bool($provision_key)
	#$st_provisioned = value.get_str($provision_key)
	$st_done = value.get_str("${name}-done")
	$provisioned = $st_done->ready and $st_done->value == "true" # export this value to parent scope

	$st_sshkey = value.get_str("${name}-sshkey")
	$sshkey = $st_sshkey->value # export this value to parent scope
}
