Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
stv0g committed Oct 27, 2024
1 parent 99b1c02 commit 37eb45e
Show file tree
Hide file tree
Showing 5 changed files with 379 additions and 0 deletions.
2 changes: 2 additions & 0 deletions pkg/cloud-init-metadata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
instance-id: h1
local-hostname: h1
4 changes: 4 additions & 0 deletions pkg/cloud-init-userdata.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
ssh_authorized_keys:
- ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBERI62l9pAbMxi6QYd3xnEMJhOY9NxcUOvgzNrJsDqSSRs5UgRjHCTDbw+7+yqr+ibcwDAcQgnzJEdRqsdhdTdc=
ssh_import_id:
- gh:stv0g
96 changes: 96 additions & 0 deletions pkg/options/vm/vm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2023 Steffen Vogel <[email protected]>
// SPDX-License-Identifier: Apache-2.0

package vm

import (
"fmt"
"strings"

g "cunicu.li/gont/v2/pkg"
co "cunicu.li/gont/v2/pkg/options/cmd"
)

type Architecture string

func (a Architecture) ApplyQEmuVM(vm *g.QEmuVM) {
vm.Arch = string(a)

Check warning on line 17 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L16-L17

Added lines #L16 - L17 were not covered by tests
}

func Option(name string, opts ...any) co.Arguments {
kvs := []string{}

for _, opt := range opts {
switch opt := opt.(type) {
case map[string]any:
for key, value := range opt {
kvs = append(kvs, fmt.Sprintf("%s=%v", key, value))
}
case map[string]string:
for key, value := range opt {
kvs = append(kvs, fmt.Sprintf("%s=%s", key, value))
}
default:
kvs = append(kvs, fmt.Sprint(opt))

Check warning on line 34 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L24-L34

Added lines #L24 - L34 were not covered by tests
}
}

args := co.Arguments{"-" + name}

if len(kvs) > 0 {
args = append(args, strings.Join(kvs, ","))
}

Check warning on line 42 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L41-L42

Added lines #L41 - L42 were not covered by tests

return args
}

type CloudInitUserData map[string]any

func (c CloudInitUserData) ApplyQEmuVM(vm *g.QEmuVM) {
vm.CloudInit.UserData = c

Check warning on line 50 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L49-L50

Added lines #L49 - L50 were not covered by tests
}

type CloudInitMetaData map[string]any

func (c CloudInitMetaData) ApplyQEmuVM(vm *g.QEmuVM) {
vm.CloudInit.MetaData = c

Check warning on line 56 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L55-L56

Added lines #L55 - L56 were not covered by tests
}

// Shortcuts

func Memory(megs int) co.Arguments {
return Option("m", map[string]any{"size": megs})

Check warning on line 62 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L61-L62

Added lines #L61 - L62 were not covered by tests
}

//nolint:gochecknoglobals
var NoGraphic = Option("nographic")

func Machine(typ string, opts ...any) co.Arguments {
args := []any{typ}
args = append(args, opts...)

return Option("machine", args...)

Check warning on line 72 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L68-L72

Added lines #L68 - L72 were not covered by tests
}

func CPU(model string) co.Arguments {
return Option("cpu", model)

Check warning on line 76 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L75-L76

Added lines #L75 - L76 were not covered by tests
}

func Device(driver string, props map[string]any) co.Arguments {
return Option("device", driver, props)

Check warning on line 80 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L79-L80

Added lines #L79 - L80 were not covered by tests
}

func NetDev(typ string, props map[string]any) co.Arguments {
return Option("netdev", typ, props)

Check warning on line 84 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L83-L84

Added lines #L83 - L84 were not covered by tests
}

func Drive(props map[string]any) co.Arguments {
return Option("drive", props)

Check warning on line 88 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L87-L88

Added lines #L87 - L88 were not covered by tests
}

func VNC(display int, opts ...any) co.Arguments {
args := []any{fmt.Sprintf(":%d", display)}
args = append(args, opts...)

return Option("vnc", args...)

Check warning on line 95 in pkg/options/vm/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/options/vm/vm.go#L91-L95

Added lines #L91 - L95 were not covered by tests
}
195 changes: 195 additions & 0 deletions pkg/vm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// SPDX-FileCopyrightText: 2023 Steffen Vogel <[email protected]>
// SPDX-License-Identifier: Apache-2.0

package gont

import (
"errors"
"fmt"
"maps"
"os"
"path/filepath"
"slices"

"gopkg.in/yaml.v3"
)

var ErrMissingCloudInitData = errors.New("missing cloud-init user data")

type QEmuVMOption interface {
ApplyQEmuVM(vm *QEmuVM)
}

var _ Node = (*QEmuVM)(nil)

type CloudInit struct {
MetaData map[string]any
UserData map[string]any
NetworkConfig map[string]any
}

type QEmuVM struct {
*BaseNode

options []any
command *Cmd

// Options
Arch string
CloudInit CloudInit
}

func (n *Network) AddQEmuVM(name string, opts ...Option) (*QEmuVM, error) {
baseNode, err := n.addBaseNode(name, opts)
if err != nil {
return nil, err
}

Check warning on line 46 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L42-L46

Added lines #L42 - L46 were not covered by tests

vm := &QEmuVM{
BaseNode: baseNode,
}

n.Register(vm)

// Apply VM options
for _, opt := range opts {
switch opt := opt.(type) {
case QEmuVMOption:
opt.ApplyQEmuVM(vm)
case ExecCmdOption, CmdOption:
vm.options = append(vm.options, opt)

Check warning on line 60 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L48-L60

Added lines #L48 - L60 were not covered by tests
}
}

// Add links
if err := vm.configureLinks(); err != nil {
return nil, fmt.Errorf("failed to configure links: %w", err)
}

Check warning on line 67 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L65-L67

Added lines #L65 - L67 were not covered by tests

return vm, nil

Check warning on line 69 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L69

Added line #L69 was not covered by tests
}

func (vm *QEmuVM) Close() error {
if vm.command.Process != nil {
if err := vm.Stop(); err != nil {
return fmt.Errorf("failed to stop VM: %w", err)
}

Check warning on line 76 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L72-L76

Added lines #L72 - L76 were not covered by tests
}

return nil

Check warning on line 79 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L79

Added line #L79 was not covered by tests
}

func (vm *QEmuVM) configureLinks() error {
for range vm.ConfiguredInterfaces {
return errors.ErrUnsupported
}

Check warning on line 85 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L82-L85

Added lines #L82 - L85 were not covered by tests

return nil

Check warning on line 87 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L87

Added line #L87 was not covered by tests
}

func (vm *QEmuVM) ConfigureInterface(_ *Interface) error {
return errors.ErrUnsupported

Check warning on line 91 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L90-L91

Added lines #L90 - L91 were not covered by tests
}

func (vm *QEmuVM) Teardown() error {
return errors.ErrUnsupported

Check warning on line 95 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L94-L95

Added lines #L94 - L95 were not covered by tests
}

func (vm *QEmuVM) Start() (*Cmd, error) {
name := fmt.Sprintf("/nix/store/v3jjx86y2cs6x9182f3d1i3z1kbap876-qemu-8.2.3/bin/qemu-system-%s", vm.Arch)

args := slices.Clone(vm.options)

if vm.CloudInit.UserData != nil {
fn, err := vm.createCloudInitImage()
if err != nil {
return nil, fmt.Errorf("failed to create cloud-init seed image: %w", err)
}

Check warning on line 107 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L98-L107

Added lines #L98 - L107 were not covered by tests

args = append(args, "-drive", fmt.Sprintf("if=virtio,format=raw,file=%s", fn))

Check warning on line 109 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L109

Added line #L109 was not covered by tests
}

vm.command = vm.network.HostNode.Command(name, args...)
vm.command.Stdin = os.Stdin
vm.command.Stderr = os.Stderr
vm.command.Stdout = os.Stdout

if err := vm.command.Start(); err != nil {
return nil, err
}

Check warning on line 119 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L112-L119

Added lines #L112 - L119 were not covered by tests

return vm.command, nil

Check warning on line 121 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L121

Added line #L121 was not covered by tests
}

func (vm *QEmuVM) Stop() error {
// TODO: Perform orderly shutdown of VM
return vm.command.Process.Kill()

Check warning on line 126 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L124-L126

Added lines #L124 - L126 were not covered by tests
}

func (vm *QEmuVM) createCloudInitImage() (string, error) {
if vm.CloudInit.UserData == nil {
return "", ErrMissingCloudInitData
}

Check warning on line 132 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L129-L132

Added lines #L129 - L132 were not covered by tests

fnOut := filepath.Join(vm.BasePath, "cloud-init.img")

args := []any{
"--disk-format", "raw",
"--filesystem", "iso9660",
fnOut,
}

out := []byte("#cloud-config\n")
outYAML, err := yaml.Marshal(vm.CloudInit.UserData)
if err != nil {
return "", fmt.Errorf("failed to marshal cloud-init metadata: %w", err)
}

Check warning on line 146 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L134-L146

Added lines #L134 - L146 were not covered by tests

out = append(out, outYAML...)

fnUser := filepath.Join(vm.BasePath, "cloud-init-userdata.yaml")
if err := os.WriteFile(fnUser, out, 0o600); err != nil {
return "", fmt.Errorf("failed to write file: %s: %w", fnUser, err)
}

Check warning on line 153 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L148-L153

Added lines #L148 - L153 were not covered by tests

args = append(args, fnUser)

if vm.CloudInit.MetaData != nil {
meta := maps.Clone(vm.CloudInit.MetaData)
meta["dsmode"] = "local"

out, err := yaml.Marshal(meta)
if err != nil {
return "", fmt.Errorf("failed to marshal cloud-init metadata: %w", err)
}

Check warning on line 164 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L155-L164

Added lines #L155 - L164 were not covered by tests

fnMeta := filepath.Join(vm.BasePath, "cloud-init-metadata.yaml")
if err := os.WriteFile(fnMeta, out, 0o600); err != nil {
return "", fmt.Errorf("failed to write file: %s: %w", fnMeta, err)
}

Check warning on line 169 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L166-L169

Added lines #L166 - L169 were not covered by tests

args = append(args, fnMeta)

Check warning on line 171 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L171

Added line #L171 was not covered by tests
}

if vm.CloudInit.NetworkConfig != nil {
out, err := yaml.Marshal(vm.CloudInit.NetworkConfig)
if err != nil {
return "", fmt.Errorf("failed to marshal cloud-init network config: %w", err)
}

Check warning on line 178 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L174-L178

Added lines #L174 - L178 were not covered by tests

fnNetCfg := filepath.Join(vm.BasePath, "cloud-init-network.yaml")
if err := os.WriteFile(fnNetCfg, out, 0o600); err != nil {
return "", fmt.Errorf("failed to write file: %s: %w", fnNetCfg, err)
}

Check warning on line 183 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L180-L183

Added lines #L180 - L183 were not covered by tests

// args = append(args, fnNetCfg)
}

cmd := vm.network.HostNode.Command("/nix/store/05dsd12j15sg8qbf7jz6dg5kv7q32z1c-cloud-utils-0.32/bin/cloud-localds", args...)
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to run cloud-localds: %w", err)
}

Check warning on line 192 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L188-L192

Added lines #L188 - L192 were not covered by tests

return fnOut, nil

Check warning on line 194 in pkg/vm.go

View check run for this annotation

Codecov / codecov/patch

pkg/vm.go#L194

Added line #L194 was not covered by tests
}
82 changes: 82 additions & 0 deletions pkg/vm_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
// SPDX-FileCopyrightText: 2023 Steffen Vogel <[email protected]>
// SPDX-License-Identifier: Apache-2.0

package gont_test

import (
"testing"

g "cunicu.li/gont/v2/pkg"
vmo "cunicu.li/gont/v2/pkg/options/vm"
"github.com/stretchr/testify/require"
)

const imageURL = "https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-generic-arm64.qcow2" //nolint:unused

// TestPing performs and end-to-end ping test
// between two hosts on a switched topology
//
// h1 <-> sw1 <-> h2
func TestQEmuVM(t *testing.T) {
t.Skip()

n, err := g.NewNetwork(*nname, globalNetworkOptions...)
require.NoError(t, err, "Failed to create network")
defer n.Close()

sw1, err := n.AddSwitch("sw1")
require.NoError(t, err, "Failed to add switch")

vmOpts := []g.Option{
vmo.Architecture("x86_64"),
vmo.Machine("q35"),
// vmo.CPU("host"),
vmo.Memory(2 * 1024),

vmo.NoGraphic,
vmo.VNC(0),

vmo.Device("virtio-blk-pci", map[string]any{"drive": "disk"}),

vmo.Device("virtio-net-pci", map[string]any{"netdev": "net0"}),
vmo.NetDev("tap", map[string]any{"id": "net0", "script": "no", "downscript": "no"}),
vmo.Drive(map[string]any{"if": "none", "id": "disk", "format": "qcow2", "file": "/home/stv0g/workspace/cunicu/gont/image.qcow2"}),

g.NewInterface("veth0", sw1),

vmo.CloudInitMetaData{
"instance-id": "h1",
"local-hostname": "h1",
},

vmo.CloudInitUserData{
"ssh_pwauth": true,
"users": []any{
"default",
map[string]any{
"name": "stv0g",
"ssh_authorized_keys": []string{
"ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBERI62l9pAbMxi6QYd3xnEMJhOY9NxcUOvgzNrJsDqSSRs5UgRjHCTDbw+7+yqr+ibcwDAcQgnzJEdRqsdhdTdc=",
},
"ssh_import_id": []string{
"gh:stv0g",
},
"groups": []string{"users", "admin", "wheel"},
"plain_text_passwd": "testtest",
},
},
},
}

h1, err := n.AddQEmuVM("h1", vmOpts...)
require.NoError(t, err, "Failed to add VM")

// h2, err := n.AddQEmuVM("h2", vmOpts...)
// require.NoError(t, err, "Failed to add VM")

cmd, err := h1.Start()
require.NoError(t, err)

err = cmd.Wait()
require.NoError(t, err)
}

0 comments on commit 37eb45e

Please sign in to comment.