lang: core: embedded: provisioner: Implement handoff

Here's a good first way to implement handoff. What's particularly
elegant about handoff here, is that this is the first form of it I know,
where handoff happens between a provisioning tool and a configuration
management tool and those are the same tool! As a result, this can allow
for some really elegant integration, and the end-user never has to deal
with the combinatorial explosion of the N * M scenario of gluing each
provisioning tool to each different configuration management tool.

We'll have other forms of handoff in the future, but this simple
approach is useful already.
This commit is contained in:
James Shubin
2024-10-29 16:36:06 -04:00
parent 93eb8b2b76
commit 3c665174cc
3 changed files with 178 additions and 2 deletions

View File

@@ -34,7 +34,9 @@ import "convert"
import "deploy"
import "fmt"
import "golang"
import "golang/path/filepath"
import "golang/strings"
import "list"
import "local"
import "net"
import "os"
@@ -524,6 +526,10 @@ class base:host($name, $config) {
# should we provision this host by default?
$provision_default = $config->provision || false # false is safest!
$handoff_type = $config->handoff || ""
$handoff_code = $config->handoff_code || ""
panic(not strings.has_prefix($handoff_code, "/"))
# unique host key which is usually a mac address unless it's a default
$hkey = if $mac == "" {
"default"
@@ -632,6 +638,103 @@ class base:host($name, $config) {
}
}
# If it's a dir we don't need a suffix, otherwise return the last chunk.
$handoff_code_chunk = if strings.has_suffix($prefix, "/") {
""
} else {
filepath.base($handoff_code)
}
if $handoff_code != "" { # it's a file path or dir!
$abs_tar = "${vardir}deploys/deploy-${provision_key}.tar"
$abs_gz = "${abs_tar}.gz"
# Tag this so that the folder purge doesn't remove it. (XXX: bug!)
file "${abs_tar}" {
owner => "root",
group => "root",
mode => "u=rw,g=rw,o=", # file
Meta:retry => -1, # changing the mode on this file can be racy
}
file "${abs_gz}" {
owner => "root",
group => "root",
mode => "u=rw,g=rw,o=", # file
Meta:retry => -1, # changing the mode on this file can be racy
}
tar "${abs_tar}" {
inputs => [
$handoff_code, # code comes in here!
],
Before => Gzip["${abs_gz}"],
Depend => File["${vardir}deploys/"], # make the dir first!
}
gzip "${abs_gz}" {
input => "${abs_tar}",
}
http:file "/mgmt/deploy-${provision_key}.tar.gz" {
path => "${abs_gz}",
}
}
$handoff_binary_path = "/usr/local/bin/mgmt" # we install it here
$firstboot_scripts_dir = "/var/lib/mgmt-firstboot/" # TODO: /usr/lib/ instead?
$firstboot_done_dir = "/var/lib/mgmt-firstboot/done/"
$deploy_dir = "/root/mgmt-deploy/" # deploy code dir
# TODO: we can customize these more precisely based on $handoff_type
$handoff_packages = deploy.bootstrap_packages($distro) # TODO: catch errors here with || []
panic($handoff_type != "" and len($handoff_packages) == 0)
$handoff_binary = if $handoff_type == "" {
""
} else {
# Copy over the actual mgmt binary. This enables a lot below...
"/usr/bin/wget -O '${handoff_binary_path}' 'http://${router_ip}:${http_port_str}/mgmt/binary' && /usr/bin/chmod u+x '${handoff_binary_path}'"
}
$handoff_cpcode = if $handoff_type == "" {
""
} else {
# Download a tar ball of our code.
# TODO: Alternate mechanisms of getting the code are possible.
if $handoff_code != "" {
"/usr/bin/wget -O /root/mgmt-deploy.tar.gz 'http://${router_ip}:${http_port_str}/mgmt/deploy-${provision_key}.tar.gz' && /usr/bin/mkdir '${deploy_dir}' && /usr/bin/tar -xf /root/mgmt-deploy.tar.gz --directory '${deploy_dir}'"
} else {
"/usr/bin/wget -O /root/mgmt-deploy.tar.gz 'http://${router_ip}:${http_port_str}/mgmt/deploy.tar.gz' && /usr/bin/mkdir '${deploy_dir}' && /usr/bin/tar -xf /root/mgmt-deploy.tar.gz --directory '${deploy_dir}'"
}
}
$handoff_service = if $handoff_type == "" {
""
} else {
# Setup the mgmt service, which starts on firstboot.
"${handoff_binary_path} setup svc --binary-path='${handoff_binary_path}' --install --enable"
}
$handoff_firstboot = if $handoff_type == "" {
""
} else {
# Setup the firstboot service itself.
"${handoff_binary_path} setup firstboot --binary-path='${handoff_binary_path}' --mkdir --install --enable --scripts-dir='${firstboot_scripts_dir}' --done-dir='${firstboot_done_dir}'"
}
$handoff_deploy = if $handoff_type == "" {
""
} else {
# Add a script that will run by our firstboot service on boot.
# It seems that the deploy will hang until mgmt is started...
# NOTE: We need to add the $handoff_code arg the same way it was
# passed into the provisioner. It's just now in a deploy subdir.
# If it's a dir, then this becomes the empty strings.
# XXX: The deploy could instead happen over the network to etcd.
"echo '#!/bin/bash' > ${firstboot_scripts_dir}mgmt-deploy.sh && echo '${handoff_binary_path} deploy lang --seeds=http://127.0.0.1:2379 --no-git ${deploy_dir}${handoff_code_chunk}' >> ${firstboot_scripts_dir}mgmt-deploy.sh && chmod u+x ${firstboot_scripts_dir}mgmt-deploy.sh"
}
# TODO: Do we want to signal an http:flag if we're a "default" host?
$provisioning_done = if $provision_key == "default" {
""
} else {
"/usr/bin/wget --post-data 'done=true&password=sha1TODO' -O - 'http://${router_ip}:${http_port_str}/action/done/mac=${provision_key}'"
}
$http_kickstart_template = struct{
comment => "hello!",
lang => [
@@ -652,9 +755,16 @@ class base:host($name, $config) {
# "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,
# We need $handoff_packages installed in the _KICKSTART_ environ
# so that we can actually run mgmt to do our work for us below!
packages => list.concat($packages, $handoff_packages),
post => [
"/usr/bin/wget --post-data 'done=true&password=sha1TODO' -O - 'http://${router_ip}:${http_port_str}/action/done/mac=${provision_key}'",
$handoff_binary, # copy over the binary
$handoff_cpcode, # copy over a bundle of code
$handoff_service, # install a service for mgmt
$handoff_firstboot, # install firstboot service
$handoff_deploy, # install a firstboot script to deploy
$provisioning_done, # send a done signal back here
],
}

View File

@@ -167,6 +167,10 @@ type localArgs struct {
// other or the base installation packages.
Packages []string `arg:"--packages" help:"list of additional distro packages to install (comma separated)" func:"cli_packages"`
// HandoffCode specifies that we want to handoff to this machine with a
// static code deploy bolus. This is useful for isolated, one-time runs.
HandoffCode string `arg:"--handoff-code" help:"code dir to handoff to host" func:"cli_handoff_code"` // eg: /etc/mgmt/
// OnlyUnify tells the compiler to stop after type unification. This is
// used for testing.
OnlyUnify bool `arg:"--only-unify" help:"stop after type unification"`
@@ -362,6 +366,60 @@ func (obj *provisioner) Customize(a interface{}) (*cli.RunArgs, error) {
obj.init.Logf("packages: %+v", strings.Join(obj.localArgs.Packages, ","))
}
if p := obj.localArgs.HandoffCode; p != "" {
if strings.HasPrefix(p, "~") {
expanded, err := util.ExpandHome(p)
if err != nil {
return nil, err
}
obj.localArgs.HandoffCode = expanded
}
// Make path absolute.
if !strings.HasPrefix(obj.localArgs.HandoffCode, "/") {
dir, err := os.Getwd()
if err != nil {
return nil, err
}
dir = dir + "/" // dir's should have a trailing slash!
obj.localArgs.HandoffCode = dir + obj.localArgs.HandoffCode
}
// Does this path actually exist?
if _, err := os.Stat(obj.localArgs.HandoffCode); err != nil {
return nil, err
}
binary, err := util.ExecutablePath() // path to mgmt binary
if err != nil {
return nil, err
}
// Type check this path before we provision?
out := ""
cmdOpts := &util.SimpleCmdOpts{
Debug: true,
Logf: func(format string, v ...interface{}) {
// XXX: HACK to make output more beautiful!
errorText := "cli parse error: "
s := fmt.Sprintf(format+"\n", v...)
for _, x := range strings.Split(s, "\n") {
if !strings.HasPrefix(x, errorText) {
continue
}
out = strings.TrimPrefix(x, errorText)
}
},
}
// TODO: Add a --quiet flag instead of the above filter hack.
cmdArgs := []string{"run", "--tmp-prefix", "lang", "--only-unify", obj.localArgs.HandoffCode}
if err := util.SimpleCmd(ctx, binary, cmdArgs, cmdOpts); err != nil {
return nil, fmt.Errorf("handoff code didn't type check: %s", out)
}
obj.init.Logf("handoff: %s", obj.localArgs.HandoffCode)
}
// 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: ")

View File

@@ -60,6 +60,11 @@ $distro = provisioner.cli_distro()
$version = provisioner.cli_version()
$arch = provisioner.cli_arch()
$uid = "${distro}${version}-${arch}" # eg: fedora39-x86_64
$handoff = if provisioner.cli_handoff_code() == "" { # TODO: check other types
""
} else {
"code" # some non-empty word
}
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,
@@ -71,6 +76,9 @@ include base.host("host0", struct{ # TODO: do we need a usable name anywhere?
part => provisioner.cli_part(),
packages => provisioner.cli_packages(),
#provision => true, # default if unspecified
handoff => $handoff, # alternatively some code word or querystring
#handoff_code => "/etc/mgmt/", # one way to do it
handoff_code => provisioner.cli_handoff_code(),
}) as host0
#if $host0.provisioned {