Skip to content

Create installation VM and run bootc install inside a VM using rootless podman #95

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
libvirt.org/go/libvirtxml v1.9008.0
sigs.k8s.io/yaml v1.4.0 // indirect
tags.cncf.io/container-device-interface v0.6.2 // indirect
)
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -764,6 +764,8 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
libvirt.org/go/libvirt v1.10002.0 h1:ZFQsv1G8HE8SYhLBqaOuxze6+f00x96khLwn54aWJnI=
libvirt.org/go/libvirt v1.10002.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ=
libvirt.org/go/libvirtxml v1.9008.0 h1:xo2U9SqUsufTFtbyjiqs6oDdF329cvtRdqttWN7eojk=
libvirt.org/go/libvirtxml v1.9008.0/go.mod h1:7Oq2BLDstLr/XtoQD8Fr3mfDNrzlI3utYKySXF2xkng=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=
Expand Down
279 changes: 279 additions & 0 deletions pkg/vm/domain/domain.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
package domain

import (
"encoding/json"
"fmt"
"io"
"os/exec"

"github.com/sirupsen/logrus"
"libvirt.org/go/libvirtxml"
)

type DomainOption func(d *libvirtxml.Domain)

const (
MemoryMemfd = "memfd"
MemoryAccessModeShared = "shared"
)

type DiskDriverType string

func (d DiskDriverType) String() string {
return string(d)
}

const (
DiskDriverQCOW2 DiskDriverType = "qcow2"
DiskDriverRaw DiskDriverType = "raw"
)

type DiskBus string

func (b DiskBus) String() string {
return string(b)
}

const (
DiskBusSCSI DiskBus = "scsi"
DiskBusVirtio DiskBus = "virtio"
)

func NewDomain(opts ...DomainOption) *libvirtxml.Domain {
domain := &libvirtxml.Domain{}
for _, f := range opts {
f(domain)
}

return domain
}

func WithName(name string) DomainOption {
return func(d *libvirtxml.Domain) {
d.Name = name
}
}

func WithMemory(memory uint) DomainOption {
return func(d *libvirtxml.Domain) {
d.Memory = &libvirtxml.DomainMemory{
Value: memory,
Unit: "MiB",
}
}
}

func WithMemoryBackingForVirtiofs() DomainOption {
return func(d *libvirtxml.Domain) {
d.MemoryBacking = &libvirtxml.DomainMemoryBacking{
MemorySource: &libvirtxml.DomainMemorySource{Type: MemoryMemfd},
MemoryAccess: &libvirtxml.DomainMemoryAccess{Mode: MemoryAccessModeShared},
}
}
}

func WithCPUHostModel() DomainOption {
return func(d *libvirtxml.Domain) {
d.CPU = &libvirtxml.DomainCPU{
Mode: "host-model",
}
}
}

func WithVCPUs(cpus uint) DomainOption {
return func(d *libvirtxml.Domain) {
d.VCPU = &libvirtxml.DomainVCPU{Value: cpus}
}
}

func allocateDevices(d *libvirtxml.Domain) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider introducing a WithDevices helper to eliminate repeated device allocation and append logic in each With* function.

// Add this helper to collapse the allocateDevices + append boilerplate:
func WithDevices(fn func(devs *libvirtxml.DomainDeviceList)) DomainOption {
    return func(d *libvirtxml.Domain) {
        if d.Devices == nil {
            d.Devices = &libvirtxml.DomainDeviceList{}
        }
        fn(d.Devices)
    }
}

Then refactor each With* to use it. For example, replace:

func WithFilesystem(source, target string) DomainOption {
    return func(d *libvirtxml.Domain) {
        allocateDevices(d)
        d.Devices.Filesystems = append(d.Devices.Filesystems, libvirtxml.DomainFilesystem{
            Driver: &libvirtxml.DomainFilesystemDriver{Type: "virtiofs"},
            Source: &libvirtxml.DomainFilesystemSource{Mount: &libvirtxml.DomainFilesystemSourceMount{Dir: source}},
            Target: &libvirtxml.DomainFilesystemTarget{Dir: target},
        })
    }
}

with:

func WithFilesystem(source, target string) DomainOption {
    return WithDevices(func(devs *libvirtxml.DomainDeviceList) {
        devs.Filesystems = append(devs.Filesystems, libvirtxml.DomainFilesystem{
            Driver: &libvirtxml.DomainFilesystemDriver{Type: "virtiofs"},
            Source: &libvirtxml.DomainFilesystemSource{Mount: &libvirtxml.DomainFilesystemSourceMount{Dir: source}},
            Target: &libvirtxml.DomainFilesystemTarget{Dir: target},
        })
    })
}

And similarly for WithDisk, WithSerialConsole, WithInterface, and WithVSOCK:

func WithDisk(path, serial, dev string, diskType DiskDriverType, bus DiskBus) DomainOption {
    return WithDevices(func(devs *libvirtxml.DomainDeviceList) {
        devs.Disks = append(devs.Disks, libvirtxml.DomainDisk{
            Device: "disk",
            Driver: &libvirtxml.DomainDiskDriver{Name: "qemu", Type: diskType.String()},
            Source: &libvirtxml.DomainDiskSource{File: &libvirtxml.DomainDiskSourceFile{File: path}},
            Target: &libvirtxml.DomainDiskTarget{Bus: bus.String(), Dev: dev},
            Serial: serial,
        })
    })
}

This cuts out the repeated allocateDevices(d) and anonymous wrapper in each With* function, reducing boilerplate while preserving all behavior.

if d.Devices == nil {
d.Devices = &libvirtxml.DomainDeviceList{}
}
}

func WithFilesystem(source, target, vfsdBin string) DomainOption {
return func(d *libvirtxml.Domain) {
allocateDevices(d)
d.Devices.Filesystems = append(d.Devices.Filesystems, libvirtxml.DomainFilesystem{
Driver: &libvirtxml.DomainFilesystemDriver{
Type: "virtiofs",
},
Source: &libvirtxml.DomainFilesystemSource{
Mount: &libvirtxml.DomainFilesystemSourceMount{
Dir: source,
},
},
Target: &libvirtxml.DomainFilesystemTarget{
Dir: target,
},
Binary: &libvirtxml.DomainFilesystemBinary{
Path: vfsdBin,
},
})
}
}

func WithDisk(path, serial, dev string, diskType DiskDriverType, bus DiskBus) DomainOption {
return func(d *libvirtxml.Domain) {
allocateDevices(d)
d.Devices.Disks = append(d.Devices.Disks, libvirtxml.DomainDisk{
Device: "disk",
Driver: &libvirtxml.DomainDiskDriver{
Name: "qemu",
Type: diskType.String(),
},
Source: &libvirtxml.DomainDiskSource{
File: &libvirtxml.DomainDiskSourceFile{
File: path,
},
},
Target: &libvirtxml.DomainDiskTarget{
Bus: bus.String(),
Dev: dev,
},
Serial: serial,
})
}
}

func WithSerialConsole() DomainOption {
return func(d *libvirtxml.Domain) {
allocateDevices(d)
d.Devices.Consoles = append(d.Devices.Consoles, libvirtxml.DomainConsole{
Source: &libvirtxml.DomainChardevSource{Pty: &libvirtxml.DomainChardevSourcePty{}},
Target: &libvirtxml.DomainConsoleTarget{
Type: "serial",
},
})

}
}

func WithInterface(mac, model string) DomainOption {
return func(d *libvirtxml.Domain) {
allocateDevices(d)
d.Devices.Interfaces = append(d.Devices.Interfaces, libvirtxml.DomainInterface{
Source: &libvirtxml.DomainInterfaceSource{
User: &libvirtxml.DomainInterfaceSourceUser{},
},
MAC: &libvirtxml.DomainInterfaceMAC{
Address: mac,
},
Model: &libvirtxml.DomainInterfaceModel{
Type: model,
},
})
}
}

func WithVSOCK(cid uint) DomainOption {
return func(d *libvirtxml.Domain) {
allocateDevices(d)
d.Devices.VSock = &libvirtxml.DomainVSock{
Model: "virtio",
CID: &libvirtxml.DomainVSockCID{
Address: fmt.Sprintf("%d", cid),
},
}
}
}

func WithUUID(uuid string) DomainOption {
return func(d *libvirtxml.Domain) {
d.UUID = uuid
}
}

func WithKVM() DomainOption {
return func(d *libvirtxml.Domain) {
d.Type = "kvm"
}
}

func WithOS() DomainOption {
// TODO: fix this for multiarch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we omit this and have libvirt pick a default?

return func(d *libvirtxml.Domain) {
d.OS = &libvirtxml.DomainOS{
Type: &libvirtxml.DomainOSType{
Arch: "x86_64",
Machine: "q35",
Type: "hvm",
},
}
}
}

func WithDirectBoot(kernel, initrd, cmdline string) DomainOption {
return func(d *libvirtxml.Domain) {
d.OS.Kernel = kernel
d.OS.Initrd = initrd
d.OS.Cmdline = cmdline
}
}

func WithVNC(port int) DomainOption {
return func(d *libvirtxml.Domain) {
allocateDevices(d)
d.Devices.Graphics = append(d.Devices.Graphics, libvirtxml.DomainGraphic{
VNC: &libvirtxml.DomainGraphicVNC{
Port: port,
Listen: "0.0.0.0",
},
})
d.Devices.Videos = append(d.Devices.Videos, libvirtxml.DomainVideo{
Model: libvirtxml.DomainVideoModel{
Type: "vga",
},
})
}
}

func WIthFeatures() DomainOption {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WIth -> With

return func(d *libvirtxml.Domain) {
d.Features = &libvirtxml.DomainFeatureList{
ACPI: &libvirtxml.DomainFeature{},
APIC: &libvirtxml.DomainFeatureAPIC{},
}
}
}

type diskInfo struct {
Format string `json:"format"`
BackingFile string `json:"backing-filename"`
ActualSize int64 `json:"actual-size"`
VirtualSize int64 `json:"virtual-size"`
}

func GetDiskInfo(imagePath string) (DiskDriverType, error) {
path, err := exec.LookPath("qemu-img")
if err != nil {
return "", fmt.Errorf("qemu-img not found: %v\n", err)
}

args := []string{"info", imagePath, "--output", "json"}
cmd := exec.Command(path, args...)
logrus.Debugf("Execute: %s", cmd.String())
stderr, err := cmd.StderrPipe()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Using StderrPipe with cmd.Output may deadlock or lose stderr

cmd.Output() should not be used with cmd.StderrPipe() as it can cause deadlocks or missing stderr output. Use CombinedOutput() or handle stdout and stderr manually with cmd.Start().

if err != nil {
return "", fmt.Errorf("failed to get stderr for qemu-img command: %v", err)
}
out, err := cmd.Output()
if err != nil {
errout, _ := io.ReadAll(stderr)
return "", fmt.Errorf("failed to invoke qemu-img on %s: %v: %s", imagePath, err, errout)
}
info := &diskInfo{}
err = json.Unmarshal(out, info)
if err != nil {
return "", fmt.Errorf("failed to parse disk info: %v", err)
}
switch info.Format {
case "qcow2":
return DiskDriverQCOW2, nil
case "raw":
return DiskDriverRaw, nil
default:
return "", fmt.Errorf("Unsupported format: %s", info.Format)
}
}