# Mgmt # Copyright (C) James Shubin and the project contributors # Written by James Shubin 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 . # # 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" import "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 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 = 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 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 strings.has_suffix($prefix, "/") { "" } else { 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! 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 = filepath.base(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 = 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 }