diff --git a/lang/core/embedded/provisioner.go b/lang/core/embedded/provisioner.go new file mode 100644 index 000000000..4b7ce07fc --- /dev/null +++ b/lang/core/embedded/provisioner.go @@ -0,0 +1,37 @@ +// Mgmt +// Copyright (C) 2013-2024+ 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. + +//go:build embedded_provisioner + +package coreembedded + +import ( + // import so it registers + _ "github.com/purpleidea/mgmt/lang/core/embedded/provisioner" +) diff --git a/lang/core/embedded/provisioner/files/bios-menu.tmpl b/lang/core/embedded/provisioner/files/bios-menu.tmpl new file mode 100644 index 000000000..c16e6c0c7 --- /dev/null +++ b/lang/core/embedded/provisioner/files/bios-menu.tmpl @@ -0,0 +1,18 @@ +default vesamenu.c32 +prompt 1 +timeout 150 + +label kickstart +menu label ^Install {{ .distro }} {{ .version }} {{ .arch }} ( kickstart ) +menu default +kernel {{ .distro }}{{ .version }}-{{ .arch }}/vmlinuz +append initrd={{ .distro }}{{ .version }}-{{ .arch }}/initrd.img inst.stage2={{ .inst_repo_base }}releases/{{ .version }}/{{ .flavour }}/{{ .arch }}/os/ ip=dhcp inst.ks={{ .ks }} + +label manual +menu label ^Install {{ .distro }} {{ .version }} {{ .arch }} ( manual ) +kernel {{ .distro }}{{ .version }}-{{ .arch }}/vmlinuz +append initrd={{ .distro }}{{ .version }}-{{ .arch }}/initrd.img inst.stage2={{ .inst_repo_base }}releases/{{ .version }}/{{ .flavour }}/{{ .arch }}/os/ ip=dhcp + +label local +menu label Boot from ^local drive +localboot 0xffff diff --git a/lang/core/embedded/provisioner/files/kickstart.ks.tmpl b/lang/core/embedded/provisioner/files/kickstart.ks.tmpl new file mode 100644 index 000000000..48f920b05 --- /dev/null +++ b/lang/core/embedded/provisioner/files/kickstart.ks.tmpl @@ -0,0 +1,186 @@ +{{ if .comment -}} +# +# {{ .comment }} +# +{{- end }} +# +# readme +# +# All of this is based on reading docs, experimentation, and the kickstarts repo +# at: https://pagure.io/fedora-kickstarts which I am not an expert at reading. +# If you have recommendations for improvements, please let us know! Thanks. +# +# flavour: {{ .flavour }} +# part: {{ .part }} +# bios: {{ .bios }} + +# +# system +# +text # text based install (not graphical) +{{ if .lang -}} +{{- $length_minus1 := math_minus1 (len .lang | printf "%d" | golang_strconv_atoi ) -}} +lang {{ index .lang 0 }} +{{- if gt (len .lang) 1 }} --addsupport= +{{- range $i, $x := .lang -}} +{{ if eq $i 0 }}{{ continue }}{{ end }} +{{- $x -}} +{{- if lt $i $length_minus1 -}},{{- end -}} +{{- end -}} +{{- end -}} +{{- end }} +keyboard us +{{ if .timezone -}} +# System timezone +timezone {{ .timezone }} --isUtc +{{- if .ntp_servers }} --ntpservers={{ golang_strings_join .ntp_servers "," }}{{ end -}} +{{- end }} + +# +# security +# +{{ if .password -}} +# password can be crypted with: +# python3 -c 'import crypt; print(crypt.crypt("password", crypt.mksalt(crypt.METHOD_SHA512)))' +# or +# openssl passwd -6 -salt (salt can be omitted to generate one) +rootpw --iscrypted --allow-ssh {{ .password }} +{{ else }} +rootpw --iscrypted --lock +{{- end }} +selinux --enforcing +services --enabled=sshd,NetworkManager,chronyd +{{ if .sshkeys -}} +# TODO: sort in a deterministic order +{{ range $user, $pubkey := .sshkeys -}} +sshkey --username {{ $user }} "{{ $pubkey }}" +{{- end -}} +{{- end }} + +# +# networking +# +# --device=link +# specifies the first interface with its link in the up state +# --activate +# any matching devices beyond the first will also be activated +# +network --bootproto=dhcp --device=link --activate + +firewall --enabled --service=mdns + +# +# partitioning +# +# TODO: add more magic partitioning schemes +zerombr +clearpart --all --initlabel --disklabel={{ if .bios }}msdos{{ else }}gpt{{ end }} +{{ if eq .part "btrfs" -}} +autopart --type=btrfs --noswap +{{- else if eq .part "plain" -}} +autopart --type=plain +{{- else -}} +autopart --type=plain +{{- end }} + +# +# repositories +# +{{ if .url -}} +url --url="{{ .url }}" +{{- end }} + +{{ if .repos -}} +# TODO: sort in a deterministic order +{{- range $name, $baseurl := .repos }} +repo --name="{{ $name }}" --baseurl="{{ $baseurl }}" +{{- end }} +{{- end }} + +# +# packages +# +%packages +@core +@standard +@hardware-support +{{ if eq .flavour "Workstation" -}} +# Packages for Workstation: +-initial-setup +-initial-setup-gui +gnome-initial-setup +#anaconda-webui +@base-x +@fonts +@input-methods +@multimedia +@printing +-@guest-desktop-agents +#initial-setup-gui +glibc-all-langpacks +-@dial-up +-@input-methods +-@standard +# Install workstation-product-environment to resolve RhBug:1891500 +@^workstation-product-environment +# Exclude unwanted packages from @anaconda-tools group +-gfs2-utils +-reiserfs-utils +{{- else if eq .flavour "Server" -}} +# Packages for Server: +fedora-release-server +# install the default groups for the server environment since installing the environment is not working +@server-product +@headless-management +@networkmanager-submodules +@container-management +@domain-client +@guest-agents +@server-hardware-support +-initial-setup-gui +-generic-release* +{{- end }} +# User packages: +{{ range $i, $x := .packages -}} +{{ $x }} +{{ end -}} +%end + +# +# misc +# +bootloader --timeout=1 + +# make sure that initial-setup runs and lets us do all the configuration bits +firstboot --reconfig + +# +# flavour +# +{{ if eq .flavour "Workstation" -}} +%post +# Explicitly set graphical.target as default as this is how initial-setup detects which version to run +systemctl set-default graphical.target +%end +{{ else if eq .flavour "Server" -}} +%post +# setup systemd to boot to the right runlevel +echo -n "Setting default runlevel to multiuser text mode" +systemctl set-default multi-user.target +echo . +%end +{{ end -}} + +# +# post +# +%post +{{ range $i, $x := .post -}} +{{ $x }} +{{ end -}} +%end + +# +# reboot after installation +# +reboot diff --git a/lang/core/embedded/provisioner/files/uefi-menu.tmpl b/lang/core/embedded/provisioner/files/uefi-menu.tmpl new file mode 100644 index 000000000..79b336d7c --- /dev/null +++ b/lang/core/embedded/provisioner/files/uefi-menu.tmpl @@ -0,0 +1,23 @@ +function load_video { + insmod efi_gop + insmod efi_uga + insmod video_bochs + insmod video_cirrus + insmod all_video +} + +load_video +set gfxpayload=keep +insmod gzio +set default=0 +set timeout=15 + +menuentry 'Install {{ .distro }} {{ .version }} {{ .arch }} ( kickstart )' --class fedora --class gnu-linux --class gnu --class os { + linuxefi /{{ .distro }}{{ .version }}-{{ .arch }}/vmlinuz ip=dhcp inst.repo={{ .inst_repo_base }}releases/{{ .version }}/{{ .flavour }}/{{ .arch }}/os/ inst.ks={{ .ks }} + initrdefi /{{ .distro }}{{ .version }}-{{ .arch }}/initrd.img +} + +menuentry 'Install {{ .distro }} {{ .version }} {{ .arch }} ( manual )' --class fedora --class gnu-linux --class gnu --class os { + linuxefi /{{ .distro }}{{ .version }}-{{ .arch }}/vmlinuz ip=dhcp inst.repo={{ .inst_repo_base }}releases/{{ .version }}/{{ .flavour }}/{{ .arch }}/os/ + initrdefi /{{ .distro }}{{ .version }}-{{ .arch }}/initrd.img +} diff --git a/lang/core/embedded/provisioner/main.mcl b/lang/core/embedded/provisioner/main.mcl new file mode 100644 index 000000000..3860c1aa4 --- /dev/null +++ b/lang/core/embedded/provisioner/main.mcl @@ -0,0 +1,642 @@ +# Mgmt +# Copyright (C) 2013-2024+ 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/strings" +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! + + $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", "syslinux-nonlinux",] + $pkgs_uefi = ["shim-x64", "grub2-efi-x64",] + pkg $pkgs_bios { + state => "installed", + } + #pkg $pkgs_uefi { + # state => "installed", + #} + + $pkgs_kickstart = ["fedora-kickstarts", "spin-kickstarts",] + pkg $pkgs_kickstart { + state => "installed", + } + + # + # firewalld + # + if $firewalld { + firewalld "tftp" { # 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 => "60s", + 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 + } + + print "ready" { + msg => "ready to provision!", + + 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 + cmd => "/usr/bin/dnf download ${pkgs_uefi_string} --assumeyes --disablerepo=* --repofrompath ${repoidname},${repo_url} --downloaddir=${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" + tftp:file "/uefi/shim.efi" { # needs leading slash + path => $uefi_shim, # TODO: add autoedges + + Depend => Exec["uefi-extract-${uid}"], + } + #tftp:file "/uefi/grubx64.efi" { # not 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], + } + + $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], + } + + # 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/ + } + } + + # + # 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. Re-use 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! + + # 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"], + } + } + + $tftp_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, + } + + # + # 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}" + } + + if $bios { + tftp:file "${bios_menu}" { # for bios + data => template(deploy.readfile("/files/bios-menu.tmpl"), $tftp_menu_template), + } + } else { + tftp:file "${uefi_menu}" { # for uefi + # XXX: linuxefi & initrdefi VS. kernel & append ? + data => template(deploy.readfile("/files/uefi-menu.tmpl"), $tftp_menu_template), + + #Depend => Pkg[$pkgs_uefi], + #Depend => Exec["uefi-extract"], + } + } + + $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", + #}, + packages => $packages, + post => [ + "/usr/bin/wget --post-data 'done=true&password=sha1TODO' -O - 'http://${router_ip}:${http_port_str}/action/done/mac=${provision_key}'", + ], + } + + $kickstart_file = "${kickstart_http_prefix}${hkey}.ks" + file $kickstart_file { + state => $const.res.file.state.exists, + content => template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template), + } + + http:file "/fedora/kickstart/${hkey}.ks" { # usually $mac or `default` + #data => template(deploy.readfile("/files/kickstart.ks.tmpl"), $http_kickstart_template), + path => $kickstart_file, + + Before => Print["ready"], + } + + ##$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 +} diff --git a/lang/core/embedded/provisioner/metadata.yaml b/lang/core/embedded/provisioner/metadata.yaml new file mode 100644 index 000000000..5de0af8a8 --- /dev/null +++ b/lang/core/embedded/provisioner/metadata.yaml @@ -0,0 +1 @@ +#files: "files/" # these are some extra files we can use (is the default) diff --git a/lang/core/embedded/provisioner/provisioner.go b/lang/core/embedded/provisioner/provisioner.go new file mode 100644 index 000000000..af00c9765 --- /dev/null +++ b/lang/core/embedded/provisioner/provisioner.go @@ -0,0 +1,449 @@ +// Mgmt +// Copyright (C) 2013-2024+ 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. + +//go:build embedded_provisioner + +package coreprovisioner + +import ( + "context" + "embed" + "fmt" + "log" + "net" + "os" + "os/user" + "strings" + + "github.com/purpleidea/mgmt/cli" + "github.com/purpleidea/mgmt/entry" + "github.com/purpleidea/mgmt/lang/embedded" + "github.com/purpleidea/mgmt/lang/funcs/simple" + "github.com/purpleidea/mgmt/lang/types" + "github.com/purpleidea/mgmt/util" + "github.com/purpleidea/mgmt/util/errwrap" + "github.com/purpleidea/mgmt/util/password" +) + +const ( + // ModuleName is the prefix given to all the functions in this module. + ModuleName = "provisioner" + + // Version is the version number of this module. + Version = "v0.0.1" + + // Frontend is the name of the GAPI to run. + Frontend = "lang" +) + +// NOTE: Grouped like shown is better, but you _can_ do it separately... +// Remember to add more patterns for nested child folders! +// +//go:embed metadata.yaml main.mcl files/* +var fs embed.FS // grouped is better + +// NOTE: Separate as it's part of a different API and has a different function. +// +//go:embed top.mcl +var top []byte + +// localArgs is our struct which is used to modify the CLI parser. +type localArgs struct { + // Interface is the local ethernet interface to provision from. It will + // be determined automatically if not specified. + Interface *string `arg:"--interface" help:"local ethernet interface to provision from" func:"cli_interface"` // eg: enp0s31f6 or eth0 + + // Network is the ip network with cidr that we want to use for the + // provisioner. + Network *string `arg:"--network" help:"network with cidr to use" func:"cli_network"` // eg: 192.168.42.0/24 + + // Router is the ip for this machine with included cidr. It must exist + // in the chosen network. + Router *string `arg:"--router" help:"router ip for this machine with cidr" func:"cli_router"` // eg: 192.168.42.1/24 + + // DNS are the list of upstream DNS servers to use during this process. + DNS []string `arg:"--dns" help:"upstream dns servers to use" func:"cli_dns"` // eg: ["8.8.8.8", "1.1.1.1"] + + // Prefix is a directory to store some provisioner specific state such + // as cached distro packages. It can be safely deleted. If you don't + // specify this value, one will be chosen automatically. + Prefix *string `arg:"--prefix" help:"local XDG_CACHE_HOME path" func:"cli_prefix"` // eg: ~/.cache/mgmt/provisioner/ + + // Firewalld will automatically open the required ports for being a + // provisioner. By default this is enabled, but it can be disabled if + // you use a different firewall system. + Firewalld bool `arg:"--firewalld" default:"true" help:"should we open firewalld on our provisioner" func:"cli_firewalld"` + + // repo + + // Distro specifies the distribution to use. Currently only `fedora` is + // supported. + Distro string `arg:"--distro" default:"fedora" help:"distribution to use" func:"cli_distro"` + + // Version is the distribution version. This is a string, not an int. + Version string `arg:"--version" help:"distribution version" func:"cli_version"` // eg: "38" + + // Arch is the distro architecture to use. Only x86_64 and aarch64 are + // currently supported. Patches welcome. + Arch string `arg:"--arch" default:"x86_64" help:"architecture to use" func:"cli_arch"` + + // Flavour describes a flavour of distribution to provision. The value + // and what it does is highly dependent on the distro you specified. The + // default is set automatically depending on your distro variable. + Flavour *string `arg:"--flavour" help:"flavour of distribution" func:"cli_flavour"` // eg: "Workstation" or "Server" + + // Mirror is the mirror to provision from. Pick one that supports both + // rsync AND https if you want the most capable provisioner features. A + // list for fedora is at: https://admin.fedoraproject.org/mirrormanager/ + // eg: https://mirror.csclub.uwaterloo.ca/fedora/ for example. This + // points to: https://download.fedoraproject.org/pub/fedora/linux/ by + // default if unspecified, because it will automatically translate to a + // local mirror near you. + // TODO: Do we need to do a special step of checking the signature of + // the initrd or vmlinuz or the install.img file we first load? + Mirror string `arg:"--mirror" help:"https mirror for proxy provisioning" func:"cli_mirror"` + + // Rsync is the rsync to sync from. Pick one that supports both rsync + // AND https if you want the most capable provisioner features. A list + // for fedora is at: https://admin.fedoraproject.org/mirrormanager/ eg: + // rsync://mirror.csclub.uwaterloo.ca/fedora-enchilada/linux/releases/ + // for examples. Be advised that this option will likely pull down over + // 100GiB per os/arch/version combination. Consider only using `mirror`. + Rsync string `arg:"--rsync" help:"rsync mirror for full synchronization" func:"cli_rsync"` + + // host + + // Mac is the mac address of the host that we'd like to provision. If + // you omit this, than we will attempt to provision any computer which + // asks. + Mac *net.HardwareAddr `arg:"--mac" help:"mac address to provision" func:"cli_mac"` + + // IP is the address of the host to provision. It must include the /cidr + // and be contained in the above network that was specified. + IP *string `arg:"--ip" help:"ip address with cidr of the host to provision" func:"cli_ip"` // eg: "192.168.42.114/24" + + // Bios should be set true if you want to provision legacy machines. + Bios bool `arg:"--bios" help:"should we use bios or uefi" func:"cli_bios"` + + // Password is an `openssl passwd -6` salted password. If you don't + // specify this, you will be prompted to enter the actual unhashed + // password, and it will be salted and hashed for you. + Password *string `arg:"--password" help:"the 'openssl passwd -6' salted password" func:"-"` // skip auto func gen + + // Part is the magic partitioning scheme to use. At the moment you can + // either specify `plain` or `btrfs`. The default empty string will + // use the `plain` scheme. + Part string `arg:"--part" help:"partitioning scheme, read manual for details" func:"cli_part"` // eg: empty string for plain + + // Packages are a list of additional distro packages to install. It's up + // to the user to make sure they exist and don't conflict with each + // other or the base installation packages. + Packages []string `arg:"--packages" help:"list of additional distro packages to install (comma separated)" func:"cli_packages"` +} + +// provisioner is our cli parser translator and general frontend object. +type provisioner struct { + init *entry.Init + + // localArgs is a stored reference to the localArgs config struct that + // is used in the API of the command line parsing library. After it + // adds our flags and executes it, the resultant parsed values will be + // made available here where we've stored a copy. + localArgs *localArgs + + // salted password + password string +} + +// Init implements the Initable interface which lets us collect some data and +// handles from our caller. +func (obj *provisioner) Init(init *entry.Init) error { + obj.init = init // store some data/handles including logf + + return nil +} + +// Customize implements the Customizable interface which lets us manipulate the +// CLI. +func (obj *provisioner) Customize(a interface{}) (*cli.RunArgs, error) { + //if obj.init.Debug { + // obj.init.Logf("got: %T: %+v\n", a, a) // parent Args + //} + ctx := context.TODO() + + runArgs, ok := a.(*cli.RunArgs) + if !ok { + // programming error? + return nil, fmt.Errorf("received invalid struct of type: %T", a) + } + + libConfig := runArgs.Config + //var name string + var args interface{} + if cmd := runArgs.RunLang; cmd != nil { + //name = cliUtil.LookupSubcommand(obj, cmd) // "lang" // reflect.Value.Interface: cannot return value obtained from unexported field or method + args = cmd + } + //if name == "" { + // return nil, fmt.Errorf("no frontend activated") + //} + if args == nil { + return nil, fmt.Errorf("no frontend activated") + } + //if obj.init.Debug { + // obj.init.Logf("got: %T: %+v\n", args, args) // parent Args + //} + + if obj.localArgs == nil { + // programming error + return nil, fmt.Errorf("could not convert/access our struct") + } + //localArgs := *obj.localArgs // optional + + // Add custom defaults, and improve some as well. + + if s := obj.localArgs.Interface; s == nil { + devices, err := util.GetPhysicalEthernetDevices() + if err != nil { + return nil, err + } + if i := len(devices); i == 0 || i > 1 { + return nil, fmt.Errorf("couldn't guess ethernet device, got %d", i) + } + dev := devices[0] + obj.localArgs.Interface = &dev + } + obj.init.Logf("interface: %+v", *obj.localArgs.Interface) + + if s := obj.localArgs.Network; s == nil { + x := "192.168.42.0/24" + obj.localArgs.Network = &x + } + _, netIPnet, err := net.ParseCIDR(*obj.localArgs.Network) + if err != nil { + return nil, err + } + if s := obj.localArgs.Router; s == nil { + x := "192.168.42.1/24" + obj.localArgs.Router = &x + } + routerIP, _, err := net.ParseCIDR(*obj.localArgs.Router) + if err != nil { + return nil, err + } + if !netIPnet.Contains(routerIP) { + return nil, fmt.Errorf("network %s does not contain %s", *obj.localArgs.Network, *obj.localArgs.Router) + } + + if s := obj.localArgs.IP; s == nil { + x := "192.168.42.13/24" + obj.localArgs.IP = &x + } + hostIP, _, err := net.ParseCIDR(*obj.localArgs.Router) + if err != nil { + return nil, err + } + if !netIPnet.Contains(hostIP) { + return nil, fmt.Errorf("network %s does not contain %s", *obj.localArgs.Network, *obj.localArgs.IP) + } + + // TODO: add more validation + + if p := obj.localArgs.Prefix; p != nil { + if strings.HasPrefix(*p, "~") { + expanded, err := util.ExpandHome(*p) + if err != nil { + return nil, err + } + obj.localArgs.Prefix = &expanded + } + } + if obj.localArgs.Prefix == nil { // pick a default + user, err := user.Current() + if err != nil { + return nil, errwrap.Wrapf(err, "can't get current user") + } + + xdg := os.Getenv("XDG_CACHE_HOME") + // Ensure there is a / at the end of the directory path. + if xdg != "" && !strings.HasSuffix(xdg, "/") { + xdg = xdg + "/" + } + if xdg == "" && user.HomeDir != "" { + xdg = fmt.Sprintf("%s/.cache/%s/", user.HomeDir, obj.init.Data.Program) + } + + xdg += fmt.Sprintf("%s/", ModuleName) // pick a dir for this tool + obj.localArgs.Prefix = &xdg + } + obj.init.Logf("cache prefix: %+v", *obj.localArgs.Prefix) + + if obj.localArgs.Mac == nil { + mac := net.HardwareAddr([]byte{}) // will print empty string + obj.localArgs.Mac = &mac + } + + if obj.localArgs.Distro == "" { + return nil, fmt.Errorf("distro was not specified") + } + if obj.localArgs.Distro != "fedora" { // TODO: add other distros! + return nil, fmt.Errorf("only fedora is currently supported") + } + + if obj.localArgs.Distro == "fedora" && obj.localArgs.Version == "" { + version, err := util.LatestFedoraVersion(ctx, obj.localArgs.Arch) // get a default for fedora + if err != nil { + return nil, err + } + obj.localArgs.Version = version + } + if obj.localArgs.Version == "" { + return nil, fmt.Errorf("distro version was not specified") + } + + if obj.localArgs.Arch == "" { + obj.localArgs.Arch = "x86_64" + } + + if obj.localArgs.Distro == "fedora" && obj.localArgs.Flavour == nil { + flavour := "Workstation" // set a default for fedora + obj.localArgs.Flavour = &flavour + } + flavour := *obj.localArgs.Flavour + + if obj.localArgs.Distro == "fedora" && flavour != strings.Title(flavour) { + return nil, fmt.Errorf("distro flavour should be in Title case") + } + + if obj.localArgs.Distro == "fedora" && obj.localArgs.Mirror == "" { + obj.localArgs.Mirror = "https://download.fedoraproject.org/pub/fedora/linux/" // default + // This will auto-resolve once we get going. + m, err := util.GetFedoraDownloadURL(ctx) + if err == nil { + obj.localArgs.Mirror = m + } + } + + obj.init.Logf("distro uid: %s%s-%s", obj.localArgs.Distro, obj.localArgs.Version, obj.localArgs.Arch) + obj.init.Logf("flavour: %+v", flavour) + obj.init.Logf("mirror: %+v", obj.localArgs.Mirror) + if len(obj.localArgs.Packages) > 0 { + obj.init.Logf("packages: %+v", strings.Join(obj.localArgs.Packages, ",")) + } + + // Do this last to let others fail early b/c this has user interaction. + if obj.localArgs.Password == nil { + b, err := password.ReadPasswordCtxPrompt(ctx, "["+ModuleName+"] password: ") + if err != nil { + return nil, err + } + fmt.Printf("\n") // leave space after the prompt + // XXX: I have no idea if I am doing this correctly, and I have + // no idea if the library is doing this correctly. Please check! + // XXX: erase values: https://github.com/golang/go/issues/21865 + hash, err := password.SaltedSHA512Password(b) // internally salted + if err != nil { + return nil, err + } + obj.password = hash // store + } else if p := *obj.localArgs.Password; p == "-" { + // XXX: pull from a file or something else if we choose this + return nil, fmt.Errorf("not implemented") + } else if len(p) != 106 { // salted length should be 106 chars AIUI + return nil, fmt.Errorf("password must be salted with openssl passwd -6") + } else { + obj.password = p // salted + } + + // Make any changes here that we want to... + runArgs.RunLang.SkipUnify = true // speed things up for known good code + libConfig.TmpPrefix = true + libConfig.NoPgp = true + + runArgs.Config = libConfig // store any changes we made + return runArgs, nil +} + +// Register generates some functions that expose the output of our local CLI. +func (obj *provisioner) Register(moduleName string) error { + + // Build all the functions... + if err := simple.StructRegister(moduleName, obj.localArgs); err != nil { + return err + } + + // Build a few separately... + simple.ModuleRegister(moduleName, "cli_password", &types.FuncValue{ + T: types.NewType("func() str"), + V: func(input []types.Value) (types.Value, error) { + if obj.localArgs == nil { + // programming error + return nil, fmt.Errorf("could not convert/access our struct") + } + //localArgs := *obj.localArgs // optional + return &types.StrValue{ + V: obj.password, + }, nil + }, + }) + + return nil +} + +func init() { + fullModuleName := embedded.FullModuleName(ModuleName) + //fs := embedded.MergeFS(metadata, main, files) // To merge filesystems! + embedded.ModuleRegister(fullModuleName, fs) + + var a interface{} = &localArgs{} // must use the pointer here + + custom := &provisioner{ + localArgs: a.(*localArgs), // force the correct type + } + + entry.Register(&entry.Data{ + Program: ModuleName, + Version: Version, // TODO: get from git? + + Debug: false, + Logf: func(format string, v ...interface{}) { + log.Printf(format, v...) + }, + + Args: a, + Custom: custom, + + Frontend: Frontend, + Top: top, + }) + + if err := custom.Register(fullModuleName); err != nil { // functions from cli + panic(err) + } +} diff --git a/lang/core/embedded/provisioner/top.mcl b/lang/core/embedded/provisioner/top.mcl new file mode 100644 index 000000000..61391f4c4 --- /dev/null +++ b/lang/core/embedded/provisioner/top.mcl @@ -0,0 +1,80 @@ +# Mgmt +# Copyright (C) 2013-2024+ 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. + +# sudo setcap CAP_NET_BIND_SERVICE=+eip mgmt +# ./mgmt provisioner --mac 01:23:45:67:89:ab + +import "fmt" +import "os" +import "golang/strings" +import "embedded/provisioner" # embedded import + +# TODO: get all of the values first from the cli config file, and then a webui +include provisioner.base(struct{ + interface => provisioner.cli_interface(), + network => provisioner.cli_network(), + router => provisioner.cli_router(), + dns => provisioner.cli_dns(), + + prefix => provisioner.cli_prefix(), + firewalld => provisioner.cli_firewalld(), +}) as base + +include base.repo(struct{ + distro => provisioner.cli_distro(), + version => provisioner.cli_version(), # not an int! + arch => provisioner.cli_arch(), + flavour => provisioner.cli_flavour(), + + # pick one from: https://admin.fedoraproject.org/mirrormanager/ + mirror => provisioner.cli_mirror(), # eg: https://mirror.csclub.uwaterloo.ca/fedora/linux/ + rsync => provisioner.cli_rsync(), # eg: rsync://mirror.csclub.uwaterloo.ca/fedora-enchilada/linux/ +}) #as repo +$distro = provisioner.cli_distro() +$version = provisioner.cli_version() +$arch = provisioner.cli_arch() +$uid = "${distro}${version}-${arch}" # eg: fedora39-x86_64 +include base.host("host0", struct{ # TODO: do we need a usable name anywhere? + #repo => $repo.uid, # type unification performance is very slow here + repo => $uid, + flavour => provisioner.cli_flavour(), + mac => provisioner.cli_mac(), + ip => provisioner.cli_ip(), + bios => provisioner.cli_bios(), # false or absent means use uefi + password => provisioner.cli_password(), # openssl passwd -6 + part => provisioner.cli_part(), + packages => provisioner.cli_packages(), + #provision => true, # default if unspecified +}) as host0 + +#if $host0.provisioned { +# print "provisioned" { +# msg => fmt.printf("%s is provisioned!", $host0.name), +# } +#}