lang: core: embedded: Add standalone provisioning tool
This commit adds the ability to build a standalone provisioning tool. This is the first useful public mcl code base as well. It is not perfect, but does serve as a rough starting point to show what is possible. In the future as the language and the engine evolve, this will likely get more elegant, and also grow new features. To build this, run `make clean && GOTAGS='embedded_provisioner' make`. To run this, run `mgmt provisioner`.
This commit is contained in:
37
lang/core/embedded/provisioner.go
Normal file
37
lang/core/embedded/provisioner.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2024+ 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.
|
||||
|
||||
//go:build embedded_provisioner
|
||||
|
||||
package coreembedded
|
||||
|
||||
import (
|
||||
// import so it registers
|
||||
_ "github.com/purpleidea/mgmt/lang/core/embedded/provisioner"
|
||||
)
|
||||
18
lang/core/embedded/provisioner/files/bios-menu.tmpl
Normal file
18
lang/core/embedded/provisioner/files/bios-menu.tmpl
Normal file
@@ -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
|
||||
186
lang/core/embedded/provisioner/files/kickstart.ks.tmpl
Normal file
186
lang/core/embedded/provisioner/files/kickstart.ks.tmpl
Normal file
@@ -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 <YOUR_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
|
||||
23
lang/core/embedded/provisioner/files/uefi-menu.tmpl
Normal file
23
lang/core/embedded/provisioner/files/uefi-menu.tmpl
Normal file
@@ -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
|
||||
}
|
||||
642
lang/core/embedded/provisioner/main.mcl
Normal file
642
lang/core/embedded/provisioner/main.mcl
Normal file
@@ -0,0 +1,642 @@
|
||||
# Mgmt
|
||||
# Copyright (C) 2013-2024+ 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/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
|
||||
}
|
||||
1
lang/core/embedded/provisioner/metadata.yaml
Normal file
1
lang/core/embedded/provisioner/metadata.yaml
Normal file
@@ -0,0 +1 @@
|
||||
#files: "files/" # these are some extra files we can use (is the default)
|
||||
449
lang/core/embedded/provisioner/provisioner.go
Normal file
449
lang/core/embedded/provisioner/provisioner.go
Normal file
@@ -0,0 +1,449 @@
|
||||
// Mgmt
|
||||
// Copyright (C) 2013-2024+ 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.
|
||||
|
||||
//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)
|
||||
}
|
||||
}
|
||||
80
lang/core/embedded/provisioner/top.mcl
Normal file
80
lang/core/embedded/provisioner/top.mcl
Normal file
@@ -0,0 +1,80 @@
|
||||
# Mgmt
|
||||
# Copyright (C) 2013-2024+ 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.
|
||||
|
||||
# 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),
|
||||
# }
|
||||
#}
|
||||
Reference in New Issue
Block a user