engine: resources: Add a virt-builder resource
This wraps the excellent virt-builder utility which makes setting up new virtual machines a breeze.
This commit is contained in:
752
engine/resources/virt_builder.go
Normal file
752
engine/resources/virt_builder.go
Normal file
@@ -0,0 +1,752 @@
|
||||
// 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.
|
||||
|
||||
package resources
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/purpleidea/mgmt/engine"
|
||||
"github.com/purpleidea/mgmt/engine/traits"
|
||||
archUtil "github.com/purpleidea/mgmt/util/arch"
|
||||
"github.com/purpleidea/mgmt/util/errwrap"
|
||||
"github.com/purpleidea/mgmt/util/recwatch"
|
||||
)
|
||||
|
||||
func init() {
|
||||
engine.RegisterResource("virt:builder", func() engine.Res { return &VirtBuilderRes{} })
|
||||
|
||||
if !strings.HasPrefix(VirtBuilderBinDir, "/") {
|
||||
panic("the VirtBuilderBinDir does not start with a slash")
|
||||
}
|
||||
if !strings.HasSuffix(VirtBuilderBinDir, "/") {
|
||||
panic("the VirtBuilderBinDir does not end with a slash")
|
||||
}
|
||||
virtBuilderMutex = &sync.Mutex{}
|
||||
}
|
||||
|
||||
var (
|
||||
// virtBuilderMutex is a mutex for virt:builder res global operations.
|
||||
virtBuilderMutex *sync.Mutex
|
||||
)
|
||||
|
||||
const (
|
||||
// VirtBuilderCmdPath is the path to the virt-builder binary. For now,
|
||||
// this is the same on any Linux machine, so it's a constant.
|
||||
VirtBuilderCmdPath = "/usr/bin/virt-builder"
|
||||
|
||||
// VirtBuilderBinDir is the directory to copy our binary to in the vm.
|
||||
VirtBuilderBinDir = "/usr/local/bin/"
|
||||
)
|
||||
|
||||
// VirtBuilderRes is a resource for building virtual machine images. It is based
|
||||
// on the amazing virt-builder tool which is part of the guestfs suite of tools.
|
||||
// TODO: Add autoedges with the virt resource disk path!
|
||||
type VirtBuilderRes struct {
|
||||
traits.Base // add the base methods without re-implementation
|
||||
|
||||
init *engine.Init
|
||||
|
||||
// Output is the full absolute file path where the image will be
|
||||
// created. If this file exists, then no action will be performed.
|
||||
// TODO: Consider adding a "overwrite" type mechanism in the future,
|
||||
// when we can find a safe way to do so.
|
||||
Output string `lang:"output" yaml:"output"`
|
||||
|
||||
// OSVersion specifies which distro and version to use for installation.
|
||||
// You will need to pick from the output of `virt-builder --list`.
|
||||
OSVersion string `lang:"os_version" yaml:"os_version"`
|
||||
|
||||
// Arch specifies the CPU architecture to use for this machine. You will
|
||||
// need to pick from the output of `virt-builder --list`. Note that not
|
||||
// all OSVersion+Arch combinations may exist.
|
||||
Arch string `lang:"arch" yaml:"arch"`
|
||||
|
||||
// Hostname for the new machine.
|
||||
Hostname string `lang:"hostname" yaml:"hostname"`
|
||||
|
||||
// Format is the disk image format. You likely want "raw" or "qcow2".
|
||||
Format string `lang:"format" yaml:"format"`
|
||||
|
||||
// Size is the disk size of the new virtual machine in bytes.
|
||||
Size int `lang:"size" yaml:"size"`
|
||||
|
||||
// Packages is the list of packages to install. If Bootstrap is true,
|
||||
// then it will add additional packages that we install if needed.
|
||||
Packages []string `lang:"packages" yaml:"packages"`
|
||||
|
||||
// Update specifies that we should update the installed packages during
|
||||
// image build. This defaults to true.
|
||||
Update bool `lang:"update" yaml:"update"`
|
||||
|
||||
// SelinuxRelabel specifies that we should do an selinux relabel on the
|
||||
// final image. This defaults to true.
|
||||
SelinuxRelabel bool `lang:"selinux_relabel" yaml:"selinux_relabel"`
|
||||
|
||||
// NoSetup can be set to true to disable trying to install the package
|
||||
// for the virt-builder binary.
|
||||
NoSetup bool `lang:"no_setup" yaml:"no_setup"`
|
||||
|
||||
// SSHKeys is a list of additional keys to add to the machine. This is
|
||||
// not a map because you may wish to add more than one to that user
|
||||
// account.
|
||||
SSHKeys []*SSHKeyInfo `lang:"ssh_keys" yaml:"ssh_keys"`
|
||||
|
||||
// RootSSHInject disables installing the root ssh key into the new vm.
|
||||
// If one is not present, then nothing is done. This defaults to true.
|
||||
RootSSHInject bool `lang:"root_ssh_inject" yaml:"root_ssh_inject"`
|
||||
|
||||
// Bootstrap can be set to false to disable any automatic bootstrapping
|
||||
// of running the mgmt binary on first boot. If this is set, we will
|
||||
// attempt to copy the mgmt binary in, and then run it. This also adds
|
||||
// additional packages to install which are needed to bootstrap mgmt.
|
||||
// This defaults to true.
|
||||
// TODO: This does not yet support multi or cross arch.
|
||||
// FIXME: This doesn't kick off mgmt runs yet.
|
||||
Bootstrap bool `lang:"bootstrap" yaml:"bootstrap"`
|
||||
|
||||
// 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
|
||||
// in these logs in cleartext!
|
||||
LogOutput bool `lang:"log_output" yaml:"log_output"`
|
||||
|
||||
varDir string
|
||||
}
|
||||
|
||||
// getOutput returns the output filename of the image that we plan to build. If
|
||||
// the Output field is set, we use that, otherwise we use the Name.
|
||||
func (obj *VirtBuilderRes) getOutput() string {
|
||||
if obj.Output != "" {
|
||||
return obj.Output
|
||||
}
|
||||
return obj.Name()
|
||||
}
|
||||
|
||||
// getArch returns the architecture that we want to use. If not specified, or if
|
||||
// we don't have a mapping for it, we return the empty string.
|
||||
func (obj *VirtBuilderRes) getArch() string {
|
||||
if obj.Arch != "" {
|
||||
return obj.Arch
|
||||
}
|
||||
|
||||
defaultArch, exists := archUtil.GoArchToVirtBuilderArch(runtime.GOARCH)
|
||||
if !exists {
|
||||
return ""
|
||||
}
|
||||
|
||||
return defaultArch
|
||||
}
|
||||
|
||||
// getBinaryPath returns the path to the binary of this program.
|
||||
func (obj *VirtBuilderRes) getBinaryPath() (string, error) {
|
||||
p1, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
p2, err := filepath.EvalSymlinks(p1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
p3, err := filepath.Abs(p2)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return p3, nil
|
||||
}
|
||||
|
||||
// getGuestfs returns the package to install for our os so that we can run
|
||||
// virt-builder.
|
||||
func (obj *VirtBuilderRes) getGuestfs() (string, error) {
|
||||
// TODO: Improve this function as things evolve.
|
||||
|
||||
if _, err := os.Stat("/etc/redhat-release"); err == nil { // fedora
|
||||
return "guestfs-tools", nil
|
||||
}
|
||||
|
||||
if _, err := os.Stat("/etc/debian_version"); err == nil { // debian
|
||||
return "guestfs-tools", nil
|
||||
}
|
||||
|
||||
// TODO: patches welcome!
|
||||
return "", fmt.Errorf("os/version is not supported")
|
||||
}
|
||||
|
||||
// getDeps returns a list of packages to install for the specific os-version so
|
||||
// that we can easily run mgmt.
|
||||
func (obj *VirtBuilderRes) getDeps() ([]string, error) {
|
||||
// TODO: Improve this function as things evolve.
|
||||
|
||||
if strings.HasPrefix(obj.OSVersion, "fedora-") {
|
||||
return []string{
|
||||
"augeas-devel",
|
||||
"libvirt-devel",
|
||||
"PackageKit",
|
||||
}, nil
|
||||
}
|
||||
|
||||
if strings.HasPrefix(obj.OSVersion, "debian-") {
|
||||
return []string{
|
||||
"libaugeas-dev",
|
||||
"libvirt-dev",
|
||||
"packagekit-tools",
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TODO: patches welcome!
|
||||
return nil, fmt.Errorf("os version is not supported")
|
||||
}
|
||||
|
||||
// Default returns some sensible defaults for this resource.
|
||||
func (obj *VirtBuilderRes) Default() engine.Res {
|
||||
return &VirtBuilderRes{
|
||||
Update: true,
|
||||
SelinuxRelabel: true,
|
||||
RootSSHInject: true,
|
||||
Bootstrap: true,
|
||||
LogOutput: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate reports any problems with the struct definition.
|
||||
func (obj *VirtBuilderRes) Validate() error {
|
||||
if !strings.HasPrefix(obj.getOutput(), "/") {
|
||||
return fmt.Errorf("output must be absolute and start with slash")
|
||||
}
|
||||
|
||||
if obj.OSVersion == "" {
|
||||
return fmt.Errorf("must specify OSVersion")
|
||||
}
|
||||
// TODO: Check if OSVersion+Arch is in list of virt-builder --list --list-format json
|
||||
// TODO: Make this check inexpensive by caching the result in $vardir?
|
||||
|
||||
if obj.Hostname != "" {
|
||||
// TODO: Validate hostname chars
|
||||
}
|
||||
|
||||
if obj.Format != "" {
|
||||
if obj.Format != "raw" && obj.Format != "qcow2" {
|
||||
return fmt.Errorf("format must be 'raw' or 'qcow2'")
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Size < 0 {
|
||||
return fmt.Errorf("size must be positive")
|
||||
}
|
||||
|
||||
for _, x := range obj.Packages {
|
||||
if x == "" {
|
||||
return fmt.Errorf("a package cannot be the empty string")
|
||||
}
|
||||
if strings.Contains(x, ",") {
|
||||
return fmt.Errorf("a package cannot contain a comma")
|
||||
}
|
||||
}
|
||||
|
||||
for _, x := range obj.SSHKeys {
|
||||
if err := x.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Init runs some startup code for this resource.
|
||||
func (obj *VirtBuilderRes) Init(init *engine.Init) error {
|
||||
obj.init = init // save for later
|
||||
|
||||
if obj.LogOutput {
|
||||
varDir, err := obj.init.VarDir("")
|
||||
if err != nil {
|
||||
return errwrap.Wrapf(err, "could not get VarDir in Init()")
|
||||
}
|
||||
obj.varDir = varDir
|
||||
}
|
||||
|
||||
_, err := os.Stat(VirtBuilderCmdPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if obj.NoSetup { // done early
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to get the package of the binary...
|
||||
p, err := obj.getGuestfs()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
virtBuilderMutex.Lock()
|
||||
defer virtBuilderMutex.Unlock()
|
||||
|
||||
// Try to install the binary...
|
||||
obj.init.Logf("installing: %s", p)
|
||||
if err := InstallOnePackage(context.TODO(), p); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cleanup is run by the engine to clean up after the resource is done.
|
||||
func (obj *VirtBuilderRes) Cleanup() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Watch is the primary listener for this resource and it outputs events. This
|
||||
// one watches the on disk filename if it creates one, as well as the runtime
|
||||
// value the kernel has stored!
|
||||
func (obj *VirtBuilderRes) Watch(ctx context.Context) error {
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
|
||||
recurse := false // single file
|
||||
recWatcher, err := recwatch.NewRecWatcher(obj.getOutput(), recurse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer recWatcher.Close()
|
||||
|
||||
obj.init.Running() // when started, notify engine that we're running
|
||||
|
||||
var send = false // send event?
|
||||
for {
|
||||
select {
|
||||
case event, ok := <-recWatcher.Events():
|
||||
if !ok { // channel shutdown
|
||||
return fmt.Errorf("unexpected close")
|
||||
}
|
||||
if err := event.Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if obj.init.Debug { // don't access event.Body if event.Error isn't nil
|
||||
obj.init.Logf("event(%s): %v", event.Body.Name, event.Body.Op)
|
||||
}
|
||||
send = true
|
||||
|
||||
case <-ctx.Done(): // closed by the engine to signal shutdown
|
||||
return nil
|
||||
}
|
||||
|
||||
// do all our event sending all together to avoid duplicate msgs
|
||||
if send {
|
||||
send = false
|
||||
obj.init.Event() // notify engine of an event (this can block)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CheckApply checks the resource state and applies the resource if the bool
|
||||
// input is true. It returns error info and if the state check passed or not.
|
||||
func (obj *VirtBuilderRes) CheckApply(ctx context.Context, apply bool) (bool, error) {
|
||||
|
||||
if _, err := os.Stat(obj.getOutput()); err != nil && !os.IsNotExist(err) {
|
||||
return false, err // probably some permissions error
|
||||
|
||||
} else if err == nil {
|
||||
return true, nil // if file exists, we're done (for safety!)
|
||||
}
|
||||
|
||||
// File does not exist, therefore we can continue!
|
||||
|
||||
if !apply {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
cmdName := VirtBuilderCmdPath
|
||||
cmdArgs := []string{obj.OSVersion, "--output", obj.getOutput()}
|
||||
|
||||
if arch := obj.getArch(); arch != "" {
|
||||
args := []string{"--arch", arch}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
if obj.Hostname != "" {
|
||||
args := []string{"--hostname", obj.Hostname}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
if obj.Format != "" {
|
||||
args := []string{"--format", obj.Format}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
if obj.Size > 0 {
|
||||
// size in bytes if it ends with a `b`
|
||||
args := []string{"--size", strconv.Itoa(obj.Size) + "b"}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
extraPackages := []string{}
|
||||
if obj.Bootstrap {
|
||||
p, err := obj.getDeps()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
extraPackages = append(extraPackages, p...)
|
||||
}
|
||||
|
||||
if len(obj.Packages) > 0 || len(extraPackages) > 0 {
|
||||
packages := []string{} // I think the ordering _may_ matter.
|
||||
packages = append(packages, obj.Packages...)
|
||||
packages = append(packages, extraPackages...)
|
||||
args := []string{"--install", strings.Join(packages, ",")}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
if obj.Update {
|
||||
arg := "--update"
|
||||
cmdArgs = append(cmdArgs, arg)
|
||||
}
|
||||
|
||||
if obj.SelinuxRelabel {
|
||||
arg := "--selinux-relabel"
|
||||
cmdArgs = append(cmdArgs, arg)
|
||||
}
|
||||
|
||||
for _, x := range obj.SSHKeys {
|
||||
args := []string{"--ssh-inject", x.SSHInjectLine()}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
if obj.RootSSHInject {
|
||||
args := []string{"--ssh-inject", "root"}
|
||||
cmdArgs = append(cmdArgs, args...)
|
||||
}
|
||||
|
||||
// TODO: consider changing this behaviour to get password from send/recv
|
||||
passwordArgs := []string{"--root-password", "disabled"}
|
||||
cmdArgs = append(cmdArgs, passwordArgs...)
|
||||
|
||||
if obj.Bootstrap {
|
||||
p, err := obj.getBinaryPath()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
args1 := []string{"--mkdir", VirtBuilderBinDir, "--copy-in", p + ":" + VirtBuilderBinDir} // LOCALPATH:REMOTEDIR
|
||||
cmdArgs = append(cmdArgs, args1...)
|
||||
|
||||
// TODO: bootstrap mgmt based on the deploy method this ran with
|
||||
// TODO: --tmp-prefix ? --module-path ?
|
||||
//args2 := []string{"--firstboot-command", VirtBuilderBinDir+"mgmt", "run", "lang", "?"}
|
||||
//cmdArgs = append(cmdArgs, args2...)
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, cmdName, cmdArgs...)
|
||||
|
||||
// ignore signals sent to parent process (we're in our own group)
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{
|
||||
Setpgid: true,
|
||||
Pgid: 0,
|
||||
}
|
||||
|
||||
// Capture stdout and stderr together. Same as CombinedOutput() method.
|
||||
var b bytes.Buffer
|
||||
cmd.Stdout = &b
|
||||
cmd.Stderr = &b
|
||||
|
||||
obj.init.Logf("running: %s", strings.Join(cmd.Args, " "))
|
||||
start := time.Now().UnixNano()
|
||||
if err := cmd.Start(); err != nil {
|
||||
return false, errwrap.Wrapf(err, "error starting cmd")
|
||||
}
|
||||
|
||||
err := cmd.Wait() // we can unblock this with the timeout
|
||||
out := b.String()
|
||||
|
||||
p := path.Join(obj.varDir, fmt.Sprintf("%d.log", start))
|
||||
if obj.LogOutput {
|
||||
if err := os.WriteFile(p, b.Bytes(), 0600); err != nil {
|
||||
obj.init.Logf("unable to store log: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
obj.init.Logf("built image successfully!")
|
||||
return false, nil // success!
|
||||
}
|
||||
|
||||
// Delete partial/invalid/corrupt image.
|
||||
if err := os.Remove(obj.getOutput()); err != nil && !os.IsNotExist(err) {
|
||||
// Can't delete the file :/
|
||||
// XXX: This permanently breaks our resource since subsequent
|
||||
// runs will see it as in a valid state. Maybe we should add a
|
||||
// $vardir file telling future CheckApply runs that we need to
|
||||
// do a delete first? But if we can't write that file things are
|
||||
// bad anyways.
|
||||
obj.init.Logf("permanent error, can't delete partial file: %s", obj.getOutput())
|
||||
return false, errwrap.Wrapf(err, "permanent error, can't delete partial file: %s", obj.getOutput())
|
||||
} else if err == nil {
|
||||
obj.init.Logf("deleted partial output")
|
||||
}
|
||||
|
||||
exitErr, ok := err.(*exec.ExitError) // embeds an os.ProcessState
|
||||
if !ok {
|
||||
// command failed in some bad way
|
||||
return false, errwrap.Wrapf(err, "cmd failed in some bad way")
|
||||
}
|
||||
pStateSys := exitErr.Sys() // (*os.ProcessState) Sys
|
||||
wStatus, ok := pStateSys.(syscall.WaitStatus)
|
||||
if !ok {
|
||||
return false, errwrap.Wrapf(err, "could not get exit status of cmd")
|
||||
}
|
||||
exitStatus := wStatus.ExitStatus()
|
||||
if exitStatus == 0 {
|
||||
// i'm not sure if this could happen
|
||||
return false, errwrap.Wrapf(err, "unexpected cmd exit status of zero")
|
||||
}
|
||||
|
||||
obj.init.Logf("cmd: %s", strings.Join(cmd.Args, " "))
|
||||
if out == "" {
|
||||
obj.init.Logf("cmd exit status %d", exitStatus)
|
||||
} else {
|
||||
obj.init.Logf("cmd error:\n%s", out) // newline because it's long
|
||||
}
|
||||
return false, errwrap.Wrapf(err, "cmd error") // exit status will be in the error
|
||||
}
|
||||
|
||||
// Cmp compares two resources and returns an error if they are not equivalent.
|
||||
func (obj *VirtBuilderRes) Cmp(r engine.Res) error {
|
||||
// we can only compare VirtBuilderRes to others of the same resource kind
|
||||
res, ok := r.(*VirtBuilderRes)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a %s", obj.Kind())
|
||||
}
|
||||
|
||||
if obj.Output != res.Output {
|
||||
return fmt.Errorf("the Output differs")
|
||||
}
|
||||
if obj.OSVersion != res.OSVersion {
|
||||
return fmt.Errorf("the OSVersion value differs")
|
||||
}
|
||||
if obj.Arch != res.Arch {
|
||||
return fmt.Errorf("the Arch value differs")
|
||||
}
|
||||
if obj.Hostname != res.Hostname {
|
||||
return fmt.Errorf("the Hostname value differs")
|
||||
}
|
||||
if obj.Format != res.Format {
|
||||
return fmt.Errorf("the Format value differs")
|
||||
}
|
||||
if obj.Size != res.Size {
|
||||
return fmt.Errorf("the Size value differs")
|
||||
}
|
||||
|
||||
if len(obj.Packages) != len(res.Packages) {
|
||||
return fmt.Errorf("the number of Packages differs")
|
||||
}
|
||||
for i, x := range obj.Packages {
|
||||
if pkg := res.Packages[i]; x != pkg {
|
||||
return fmt.Errorf("the package at index %d differs", i)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.Update != res.Update {
|
||||
return fmt.Errorf("the Update value differs")
|
||||
}
|
||||
if obj.SelinuxRelabel != res.SelinuxRelabel {
|
||||
return fmt.Errorf("the SelinuxRelabel value differs")
|
||||
}
|
||||
|
||||
if obj.NoSetup != res.NoSetup {
|
||||
return fmt.Errorf("the NoSetup value differs")
|
||||
}
|
||||
|
||||
if len(obj.SSHKeys) != len(res.SSHKeys) {
|
||||
return fmt.Errorf("the number of Packages differs")
|
||||
}
|
||||
for i, x := range obj.SSHKeys {
|
||||
if err := res.SSHKeys[i].Cmp(x); err != nil {
|
||||
return errwrap.Wrapf(err, "the ssh key at index %d differs", i)
|
||||
}
|
||||
}
|
||||
|
||||
if obj.RootSSHInject != res.RootSSHInject {
|
||||
return fmt.Errorf("the RootSSHInject value differs")
|
||||
}
|
||||
if obj.Bootstrap != res.Bootstrap {
|
||||
return fmt.Errorf("the Bootstrap value differs")
|
||||
}
|
||||
|
||||
if obj.LogOutput != res.LogOutput {
|
||||
return fmt.Errorf("the LogOutput value differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalYAML is the custom unmarshal handler for this struct. It is
|
||||
// primarily useful for setting the defaults.
|
||||
func (obj *VirtBuilderRes) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
||||
type rawRes VirtBuilderRes // indirection to avoid infinite recursion
|
||||
|
||||
def := obj.Default() // get the default
|
||||
res, ok := def.(*VirtBuilderRes) // put in the right format
|
||||
if !ok {
|
||||
return fmt.Errorf("could not convert to VirtBuilderRes")
|
||||
}
|
||||
raw := rawRes(*res) // convert; the defaults go here
|
||||
|
||||
if err := unmarshal(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*obj = VirtBuilderRes(raw) // restore from indirection with type conversion!
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHKeyInfo stores information about user SSH keys. It is used to add entries
|
||||
// to the authorized_keys file in the target vm.
|
||||
type SSHKeyInfo struct {
|
||||
// User is the user account we want to add this key to.
|
||||
User string `lang:"user" yaml:"user"`
|
||||
|
||||
// Type of SSH key. Usually "ssh-rsa" for example.
|
||||
Type string `lang:"type" yaml:"type"`
|
||||
|
||||
// Key is the base64 encoded key.
|
||||
Key string `lang:"key" yaml:"key"`
|
||||
|
||||
// Comment is a short comment about this entry.
|
||||
Comment string `lang:"comment" yaml:"comment"`
|
||||
}
|
||||
|
||||
// AuthorizedKeyLine returns the valid line for the authorized_keys entry. Make
|
||||
// sure you've run Validate before using this.
|
||||
func (obj *SSHKeyInfo) AuthorizedKeyLine() string {
|
||||
comment := obj.Comment
|
||||
if comment == "" {
|
||||
comment = "comment" // TODO: Put something useful like user@hostname?
|
||||
}
|
||||
return fmt.Sprintf("%s %s %s", obj.Type, obj.Key, obj.Comment)
|
||||
}
|
||||
|
||||
// SSHInjectLine returns the valid arg for the --ssh-inject command. Make sure
|
||||
// you've run Validate before using this.
|
||||
func (obj *SSHKeyInfo) SSHInjectLine() string {
|
||||
user := obj.User
|
||||
if user == "" {
|
||||
user = "root" // default user
|
||||
}
|
||||
return fmt.Sprintf("%s:string:%s", user, obj.AuthorizedKeyLine()) // USER:string:KEY_STRING
|
||||
}
|
||||
|
||||
// Validate reports any problems with the struct definition.
|
||||
func (obj *SSHKeyInfo) Validate() error {
|
||||
if obj == nil {
|
||||
return fmt.Errorf("nil obj")
|
||||
}
|
||||
//if obj.User == "" { // root if not specified
|
||||
// return fmt.Errorf("empty User")
|
||||
//}
|
||||
if obj.Type == "" {
|
||||
return fmt.Errorf("empty Type")
|
||||
}
|
||||
if obj.Key == "" {
|
||||
return fmt.Errorf("empty Key")
|
||||
}
|
||||
//if obj.Comment == "" { // generate one if not specified
|
||||
// return fmt.Errorf("empty Comment")
|
||||
//}
|
||||
|
||||
// TODO: Check key type is a valid algorithm?
|
||||
// TODO: Check key length and format is sane for key type?
|
||||
|
||||
check := func(s string) error {
|
||||
fn := func(r rune) bool {
|
||||
return unicode.IsSpace(r)
|
||||
}
|
||||
if strings.ContainsFunc(s, fn) {
|
||||
return fmt.Errorf("white space chars found")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := check(obj.User); err != nil {
|
||||
return errwrap.Wrapf(err, "problem with User")
|
||||
}
|
||||
if err := check(obj.Type); err != nil {
|
||||
return errwrap.Wrapf(err, "problem with Type")
|
||||
}
|
||||
if err := check(obj.Key); err != nil {
|
||||
return errwrap.Wrapf(err, "problem with Key")
|
||||
}
|
||||
if err := check(obj.Comment); err != nil {
|
||||
return errwrap.Wrapf(err, "problem with Comment")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Cmp compares two of these and returns an error if they are not equivalent.
|
||||
func (obj *SSHKeyInfo) Cmp(x *SSHKeyInfo) 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.User != x.User {
|
||||
return fmt.Errorf("the User differs")
|
||||
}
|
||||
if obj.Type != x.Type {
|
||||
return fmt.Errorf("the Type differs")
|
||||
}
|
||||
if obj.Key != x.Key {
|
||||
return fmt.Errorf("the Key differs")
|
||||
}
|
||||
if obj.Comment != x.Comment {
|
||||
return fmt.Errorf("the Comment differs")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
11
examples/lang/virt-builder.mcl
Normal file
11
examples/lang/virt-builder.mcl
Normal file
@@ -0,0 +1,11 @@
|
||||
import "os"
|
||||
|
||||
virt:builder "/var/lib/libvirt/images/vmtest1.raw" {
|
||||
os_version => "fedora-40",
|
||||
size => 1024*1024*1024*100, # 100G
|
||||
packages => [
|
||||
"@minimal-environment",
|
||||
"screen",
|
||||
"vim-enhanced",
|
||||
],
|
||||
}
|
||||
Reference in New Issue
Block a user