engine: resources: Add simple configuration steps to virt builder

This adds some simplistic configuration management / provisioning
functionality to this virt:builder resource which makes it easier to
kick off special functionality that we might want to build.
This commit is contained in:
James Shubin
2025-03-11 00:50:02 -04:00
parent 219d25b330
commit 46035fee83
2 changed files with 168 additions and 0 deletions

View File

@@ -154,6 +154,32 @@ type VirtBuilderRes struct {
// likely be http URL's like: http://127.0.0.1:2379 or similar.
Seeds []string `lang:"seeds" yaml:"seeds"`
// Mkdir creates these directories in the guests. This happens before
// CopyIn runs. Directories must be absolute and end with a slash. Any
// intermediate directories are created, similar to how `mkdir -p`
// works.
Mkdir []string `lang:"mkdir" yaml:"mkdir"`
// CopyIn is a list of local paths to copy into the machine dest. The
// dest directory must exist for this to work. Use Mkdir if you need to
// make a directory, since that step happens earlier. All paths must be
// absolute, and directories must end with a slash. This happens before
// the RunCmd stage in case you want to create something to be used
// there.
CopyIn []*CopyIn `lang:"copy_in" yaml:"copy_in"`
// RunCmd is a sequence of commands + args (one set per list item) to
// run in the build environment. These happen after the CopyIn stage.
RunCmd []string `lang:"run_cmd" yaml:"run_cmd"`
// FirstbootCmd is a sequence of commands + args (one set per list item)
// to run once on first boot.
// TODO: Consider replacing this with the mgmt firstboot mechanism for
// consistency between this platform and other platforms that might not
// support the excellent libguestfs version of those scripts. (Make the
// logs look more homogeneous.)
FirstbootCmd []string `lang:"firstboot_cmd" yaml:"firstboot_cmd"`
// LogOutput logs the output of running this command to a file in the
// special $vardir directory. It defaults to true. Keep in mind that if
// you let virt-builder choose the password randomly, it will be output
@@ -319,6 +345,33 @@ func (obj *VirtBuilderRes) Validate() error {
}
}
for _, x := range obj.Mkdir {
if x == "" {
return fmt.Errorf("empty Mkdir entry")
}
if !strings.HasPrefix(x, "/") {
return fmt.Errorf("the Mkdir entry must be absolute")
}
if !strings.HasSuffix(x, "/") {
return fmt.Errorf("the Mkdir entry must be a directory")
}
}
for _, x := range obj.CopyIn {
if err := x.Validate(); err != nil {
return err
}
}
for _, x := range obj.RunCmd {
if x == "" {
return fmt.Errorf("empty RunCmd entry")
}
}
for _, x := range obj.FirstbootCmd {
if x == "" {
return fmt.Errorf("empty FirstbootCmd entry")
}
}
return nil
}
@@ -535,6 +588,23 @@ func (obj *VirtBuilderRes) CheckApply(ctx context.Context, apply bool) (bool, er
}
}
for _, x := range obj.Mkdir {
args := []string{"--mkdir", x}
cmdArgs = append(cmdArgs, args...)
}
for _, x := range obj.CopyIn {
args := []string{"--copy-in", x.Path + ":" + x.Dest} // LOCALPATH:REMOTEDIR
cmdArgs = append(cmdArgs, args...)
}
for _, x := range obj.RunCmd {
args := []string{"--run-command", x}
cmdArgs = append(cmdArgs, args...)
}
for _, x := range obj.FirstbootCmd {
args := []string{"--firstboot-command", x}
cmdArgs = append(cmdArgs, args...)
}
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
// ignore signals sent to parent process (we're in our own group)
@@ -683,6 +753,39 @@ func (obj *VirtBuilderRes) Cmp(r engine.Res) error {
}
}
if len(obj.Mkdir) != len(res.Mkdir) {
return fmt.Errorf("the number of Mkdir entries differs")
}
for i, x := range obj.Mkdir {
if s := res.Mkdir[i]; x != s {
return fmt.Errorf("the Mkdir entry at index %d differs", i)
}
}
if len(obj.CopyIn) != len(res.CopyIn) {
return fmt.Errorf("the number of CopyIn structs differ")
}
for i, x := range obj.CopyIn {
if err := res.CopyIn[i].Cmp(x); err != nil {
return errwrap.Wrapf(err, "the copy in struct at index %d differs", i)
}
}
if len(obj.RunCmd) != len(res.RunCmd) {
return fmt.Errorf("the number of RunCmd entries differs")
}
for i, x := range obj.RunCmd {
if s := res.RunCmd[i]; x != s {
return fmt.Errorf("the RunCmd entry at index %d differs", i)
}
}
if len(obj.FirstbootCmd) != len(res.FirstbootCmd) {
return fmt.Errorf("the number of FirstbootCmd entries differs")
}
for i, x := range obj.FirstbootCmd {
if s := res.FirstbootCmd[i]; x != s {
return fmt.Errorf("the FirstbootCmd entry at index %d differs", i)
}
}
if obj.LogOutput != res.LogOutput {
return fmt.Errorf("the LogOutput value differs")
}
@@ -821,3 +924,58 @@ func (obj *SSHKeyInfo) Cmp(x *SSHKeyInfo) error {
return nil
}
// CopyIn is a list of local paths to copy into the machine dest.
type CopyIn struct {
// Path is the local file or directory that we want to copy in.
// TODO: Add autoedges
Path string `lang:"path" yaml:"path"`
// Dest is the destination dir that the path gets copied into. This
// directory must exist.
Dest string `lang:"dest" yaml:"dest"`
}
// Validate reports any problems with the struct definition.
func (obj *CopyIn) Validate() error {
if obj == nil {
return fmt.Errorf("nil obj")
}
if obj.Path == "" {
return fmt.Errorf("empty Path")
}
if !strings.HasPrefix(obj.Path, "/") {
return fmt.Errorf("the Path must be absolute")
}
if obj.Dest == "" {
return fmt.Errorf("empty Dest")
}
if !strings.HasPrefix(obj.Dest, "/") {
return fmt.Errorf("the Dest must be absolute")
}
if !strings.HasSuffix(obj.Dest, "/") {
return fmt.Errorf("the dest must be a directory")
}
return nil
}
// Cmp compares two of these and returns an error if they are not equivalent.
func (obj *CopyIn) Cmp(x *CopyIn) error {
//if (obj == nil) != (x == nil) { // xor
// return fmt.Errorf("we differ") // redundant
//}
if obj == nil || x == nil {
// special case since we want to error if either is nil
return fmt.Errorf("can't cmp if nil")
}
if obj.Path != x.Path {
return fmt.Errorf("the Path differs")
}
if obj.Dest != x.Dest {
return fmt.Errorf("the Dest differs")
}
return nil
}

View File

@@ -99,6 +99,11 @@ class vm($name, $config) {
}
$seeds = $config->seeds || []
$mkdir = $config->mkdir || []
$copy_in = $config->copy_in || []
$run_cmd = $config->run_cmd || []
$firstboot_cmd = $config->firstboot_cmd || []
virt:builder "${filename}" {
hostname => $name,
os_version => "${distro}-${version}",
@@ -118,6 +123,11 @@ class vm($name, $config) {
root_password_selector => $root_password_selector,
seeds => $seeds,
mkdir => $mkdir,
copy_in => $copy_in,
run_cmd => $run_cmd,
firstboot_cmd => $firstboot_cmd,
# make sure key exists so that's it's available for injection!
Depend => File["/root/.ssh/id_rsa"],
Depend => Svc["virtqemud"],