lang: core: embedded: provisioner: Support exec handoff

Could be used for any tool, but mgmt is an obvious possibility.

I should check this code more, but it's roughly right and I'm sure it
will get refactored more when I build opt-in provisioning and so on.
This commit is contained in:
James Shubin
2025-06-08 21:16:54 -04:00
parent 582cea31b0
commit 777ea6115b
3 changed files with 79 additions and 36 deletions

View File

@@ -576,6 +576,7 @@ class base:host($name, $config) {
$provision_default = $config->provision || false # false is safest! $provision_default = $config->provision || false # false is safest!
$handoff_type = $config->handoff || "" $handoff_type = $config->handoff || ""
$handoff_exec = $config->handoff_exec || ""
$handoff_code = $config->handoff_code || "" $handoff_code = $config->handoff_code || ""
$handoff_module_path = $config->handoff_module_path || "" $handoff_module_path = $config->handoff_module_path || ""
panic($handoff_code != "" and not golang_strings.has_prefix($handoff_code, "/")) panic($handoff_code != "" and not golang_strings.has_prefix($handoff_code, "/"))
@@ -756,28 +757,35 @@ class base:host($name, $config) {
panic($handoff_type != "" and len($handoff_packages) == 0) panic($handoff_type != "" and len($handoff_packages) == 0)
#$handoff_packages_string = golang_strings.join($handoff_packages, " ") #$handoff_packages_string = golang_strings.join($handoff_packages, " ")
$sshkey_flag = "sshkey"
$sshkey_type = "ed25519" # TODO: support other options
$sshkey_path = "/root/.ssh/id_${sshkey_type}"
$setup_sshkey = "mkdir -p '/root/.ssh/' -m 700 && ssh-keygen -N '' -t '${sshkey_type}' -f '${sshkey_path}'" # non-interactive
# Setting up the known_hosts file isn't needed since that can be done on
# the first run of mgmt, and the initial key can come in from --ssh-url.
$handoff_binary = if $handoff_type == "" { $handoff_binary = if $handoff_type == "" {
"" ""
} else { } else {
# Copy over the actual mgmt binary. This enables a lot below... # 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}'" "/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 == "" { $handoff_cpcode = if $handoff_type == "code" {
""
} else {
# Download a tar ball of our code. # Download a tar ball of our code.
# TODO: Alternate mechanisms of getting the code are possible. # TODO: Alternate mechanisms of getting the code are possible.
if $handoff_code != "" { if $mac != "" {
"/usr/bin/wget -O /root/mgmt-deploy.tar.gz 'http://${router_ip}:${http_port_str}/mgmt/deploy-${provision_key}.tar.gz' && /usr/bin/mkdir '${deploy_dir}' && /usr/bin/tar -xf /root/mgmt-deploy.tar.gz --directory '${deploy_dir}'" "/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 { } 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}'" "/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 { } else {
""
}
$handoff_service = if $handoff_type == "code" { # TODO: maybe add it in other scenarios
# Setup the mgmt service, which starts on firstboot. # Setup the mgmt service, which starts on firstboot.
"${handoff_binary_path} setup svc --binary-path='${handoff_binary_path}' --install --enable" "${handoff_binary_path} setup svc --binary-path='${handoff_binary_path}' --install --enable"
} else {
""
} }
$handoff_firstboot = if $handoff_type == "" { $handoff_firstboot = if $handoff_type == "" {
"" ""
@@ -785,24 +793,28 @@ class base:host($name, $config) {
# Setup the firstboot service itself. # 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_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 == "" {
"" $handoff_firstboot_exec = if $handoff_type == "exec" and $handoff_exec != "" {
# Add a script that will run by our firstboot service on boot.
# This usually just runs mgmt over the network to etcd.
"echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-exec.sh && echo 'ulimit -n 16384' >> ${firstboot_scripts_dir}mgmt-exec.sh && echo '${handoff_exec}' >> ${firstboot_scripts_dir}mgmt-exec.sh && chmod u+x ${firstboot_scripts_dir}mgmt-exec.sh"
} else { } else {
""
}
$handoff_firstboot_code = if $handoff_type == "code" {
# Add a script that will run by our firstboot service on boot. # Add a script that will run by our firstboot service on boot.
# It seems that the deploy will hang until mgmt is started... # 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 # 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. # passed into the provisioner. It's just now in a deploy subdir.
# If it's a dir, then this becomes the empty strings. # If it's a dir, then this becomes the empty strings.
# XXX: The deploy could instead happen over the network to etcd. "echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-code.sh && echo '${handoff_binary_path} deploy lang --seeds=http://127.0.0.1:2379 --no-git --module-path=${deploy_dir_modules} ${deploy_dir}${handoff_code_chunk}' >> ${firstboot_scripts_dir}mgmt-code.sh && chmod u+x ${firstboot_scripts_dir}mgmt-code.sh"
"echo '#!/usr/bin/env bash' > ${firstboot_scripts_dir}mgmt-deploy.sh && echo '${handoff_binary_path} deploy lang --seeds=http://127.0.0.1:2379 --no-git --module-path=${deploy_dir_modules} ${deploy_dir}${handoff_code_chunk}' >> ${firstboot_scripts_dir}mgmt-deploy.sh && chmod u+x ${firstboot_scripts_dir}mgmt-deploy.sh" } else {
""
} }
# TODO: Do we want to signal an http:server:flag if we're a "default" host? # TODO: Do we want to signal an http:server:flag if we're a "default" host?
$provisioning_done = if $provision_key == "default" { $provisioning_done = "/usr/bin/curl --data-urlencode 'done=true' --data-urlencode 'password=sha1TODO' --data-urlencode 'sshtype=ssh-${sshkey_type}' --data-urlencode \"${sshkey_flag}=\$(cut -d ' ' -f 2 '${sshkey_path}.pub')\" --data-urlencode 'sshcomment=root@${handoff_hostname}' -o - 'http://${router_ip}:${http_port_str}/action/done/mac=${provision_key}'"
""
} 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{ $http_kickstart_template = struct{
comment => "hello!", comment => "hello!",
@@ -836,11 +848,13 @@ class base:host($name, $config) {
#], #],
hostname => $handoff_hostname, hostname => $handoff_hostname,
post => [ post => [
$setup_sshkey, # setup the ssh key
$handoff_binary, # copy over the binary $handoff_binary, # copy over the binary
$handoff_cpcode, # copy over a bundle of code $handoff_cpcode, # copy over a bundle of code
$handoff_service, # install a service for mgmt $handoff_service, # install a service for mgmt
$handoff_firstboot, # install firstboot service $handoff_firstboot, # install firstboot service
$handoff_deploy, # install a firstboot script to deploy $handoff_firstboot_exec, # install a firstboot script to exec
$handoff_firstboot_code, # install a firstboot script to deploy
$provisioning_done, # send a done signal back here $provisioning_done, # send a done signal back here
], ],
} }
@@ -871,20 +885,40 @@ class base:host($name, $config) {
##$str_true = convert.format_bool(true) ##$str_true = convert.format_bool(true)
##$str_false = convert.format_bool(false) ##$str_false = convert.format_bool(false)
#http:server:flag "${name}" { http:server:flag "${name}-done" {
# key => "done", key => "done",
# path => "/action/done/mac=${provision_key}", path => "/action/done/mac=${provision_key}",
# #mapped => {$str_true => $str_true, $str_false => $str_false,}, #mapped => {$str_true => $str_true, $str_false => $str_false,},
#} }
#kv "${name}" { http:server:flag "${name}-sshkey" {
# key => $provision_key, key => "${sshkey_flag}",
#} path => "/action/done/mac=${provision_key}",
#value "${provision_key}" { }
# #any => true, # bool
#} # TODO: rename the names of kv and value here?
#Http:Flag["${name}"].value -> Kv["${name}"].value kv "${name}-done" {
#Http:Flag["${name}"].value -> Value["${provision_key}"].any key => $provision_key,
##$st_provisioned = value.get_bool($provision_key) }
value "${name}-done" {
#any => true, # bool
}
kv "${name}-sshkey" {
key => $provision_key,
}
value "${name}-sshkey" {
#any => "",
}
Http:Server:Flag["${name}-done"].value -> Kv["${name}-done"].value
Http:Server:Flag["${name}-done"].value -> Value["${name}-done"].any
Http:Server:Flag["${name}-sshkey"].value -> Kv["${name}-sshkey"].value
Http:Server:Flag["${name}-sshkey"].value -> Value["${name}-sshkey"].any
#$st_provisioned = value.get_bool($provision_key)
#$st_provisioned = value.get_str($provision_key) #$st_provisioned = value.get_str($provision_key)
#$provisioned = $st_provisioned->ready and $st_provisioned->value == "true" # export this value to parent scope $st_done = value.get_str("${name}-done")
$provisioned = $st_done->ready and $st_done->value == "true" # export this value to parent scope
$st_sshkey = value.get_str("${name}-sshkey")
$sshkey = $st_sshkey->value # export this value to parent scope
} }

View File

@@ -167,6 +167,10 @@ type localArgs struct {
// other or the base installation packages. // other or the base installation packages.
Packages []string `arg:"--packages,separate" help:"list of additional distro packages to install" func:"cli_packages"` Packages []string `arg:"--packages,separate" help:"list of additional distro packages to install" func:"cli_packages"`
// HandoffExec specifies that we want to handoff to this machine by
// running a single exec on firstboot. Usually an `mgmt run` command.
HandoffExec string `arg:"--handoff-exec" help:"exec command to run on firstboot" func:"cli_handoff_exec"` // eg: mgmt run ...
// HandoffCode specifies that we want to handoff to this machine with a // HandoffCode specifies that we want to handoff to this machine with a
// static code deploy bolus. This is useful for isolated, one-time runs. // 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/ HandoffCode string `arg:"--handoff-code" help:"code dir to handoff to host" func:"cli_handoff_code"` // eg: /etc/mgmt/

View File

@@ -59,10 +59,14 @@ $distro = provisioner.cli_distro()
$version = provisioner.cli_version() $version = provisioner.cli_version()
$arch = provisioner.cli_arch() $arch = provisioner.cli_arch()
$uid = "${distro}${version}-${arch}" # eg: fedora39-x86_64 $uid = "${distro}${version}-${arch}" # eg: fedora39-x86_64
$handoff = if provisioner.cli_handoff_code() == "" { # TODO: check other types $handoff = if provisioner.cli_handoff_code() != "" { # TODO: check other types
"" "code"
} else { } else {
"code" # some non-empty word if provisioner.cli_handoff_exec() != "" { # TODO: check other types
"exec"
} else {
""
}
} }
include base.host("host0", struct{ # TODO: do we need a usable name anywhere? include base.host("host0", struct{ # TODO: do we need a usable name anywhere?
#repo => $repo.uid, # type unification performance is very slow here #repo => $repo.uid, # type unification performance is very slow here
@@ -76,6 +80,7 @@ include base.host("host0", struct{ # TODO: do we need a usable name anywhere?
packages => provisioner.cli_packages(), packages => provisioner.cli_packages(),
#provision => true, # default if unspecified #provision => true, # default if unspecified
handoff => $handoff, # alternatively some code word or querystring handoff => $handoff, # alternatively some code word or querystring
handoff_exec => provisioner.cli_handoff_exec(),
#handoff_code => "/etc/mgmt/", # one way to do it #handoff_code => "/etc/mgmt/", # one way to do it
handoff_code => provisioner.cli_handoff_code(), handoff_code => provisioner.cli_handoff_code(),
handoff_module_path => provisioner.cli_handoff_module_path(), handoff_module_path => provisioner.cli_handoff_module_path(),
@@ -85,6 +90,6 @@ include base.host("host0", struct{ # TODO: do we need a usable name anywhere?
#if $host0.provisioned { #if $host0.provisioned {
# print "provisioned" { # print "provisioned" {
# msg => fmt.printf("%s is provisioned!", $host0.name), # msg => fmt.printf("%s has ssh key: %s", $host0.name, $host0.sshkey),
# } # }
#} #}