Skip to content
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

fix: better handling of boot order when running v.CloudInit() method #108

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
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
211 changes: 169 additions & 42 deletions virtual_machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const (

volumeIdentifier = "cidata"
blockSize = 2048

BootOrderPfx = "order="
BootDeviceSeparator = ";"
)

// DefaultAgentWaitInterval is the polling interval when waiting for agent exec commands
Expand Down Expand Up @@ -80,10 +83,12 @@ func (v *VirtualMachine) AddTag(ctx context.Context, value string) (*Task, error
v.VirtualMachineConfig.TagsSlice = append(v.VirtualMachineConfig.TagsSlice, value)
v.VirtualMachineConfig.Tags = strings.Join(v.VirtualMachineConfig.TagsSlice, TagSeperator)

return v.Config(ctx, VirtualMachineOption{
Name: "tags",
Value: v.VirtualMachineConfig.Tags,
})
return v.Config(
ctx, VirtualMachineOption{
Name: "tags",
Value: v.VirtualMachineConfig.Tags,
},
)
}

func (v *VirtualMachine) RemoveTag(ctx context.Context, value string) (*Task, error) {
Expand All @@ -105,10 +110,12 @@ func (v *VirtualMachine) RemoveTag(ctx context.Context, value string) (*Task, er
}

v.VirtualMachineConfig.Tags = strings.Join(v.VirtualMachineConfig.TagsSlice, TagSeperator)
return v.Config(ctx, VirtualMachineOption{
Name: "tags",
Value: v.VirtualMachineConfig.Tags,
})
return v.Config(
ctx, VirtualMachineOption{
Name: "tags",
Value: v.VirtualMachineConfig.Tags,
},
)
}

func (v *VirtualMachine) SplitTags() {
Expand All @@ -119,7 +126,10 @@ func (v *VirtualMachine) SplitTags() {
// mount it as a CD-ROM to be used with nocloud cloud-init. This is NOT how proxmox expects a user to do cloud-init
// which can be found here: https://pve.proxmox.com/wiki/Cloud-Init_Support#:~:text=and%20meta.-,Cloud%2DInit%20specific%20Options,-cicustom%3A%20%5Bmeta
// If you want to use the proxmox implementation you'll need to use the cloudinit APIs https://pve.proxmox.com/pve-docs/api-viewer/index.html#/nodes/{node}/qemu/{vmid}/cloudinit
func (v *VirtualMachine) CloudInit(ctx context.Context, device, userdata, metadata, vendordata, networkconfig string) error {
func (v *VirtualMachine) CloudInit(
ctx context.Context,
device, userdata, metadata, vendordata, networkconfig string,
) error {
isoName := fmt.Sprintf(UserDataISOFormat, v.VMID)
// create userdata iso file on the local fs
iso, err := makeCloudInitISO(isoName, userdata, metadata, vendordata, networkconfig)
Expand Down Expand Up @@ -156,13 +166,15 @@ func (v *VirtualMachine) CloudInit(ctx context.Context, device, userdata, metada
return err
}

task, err = v.Config(ctx, VirtualMachineOption{
Name: device,
Value: fmt.Sprintf("%s:iso/%s,media=cdrom", storage.Name, isoName),
}, VirtualMachineOption{
Name: "boot",
Value: fmt.Sprintf("%s;%s", v.VirtualMachineConfig.Boot, device),
})
task, err = v.Config(
ctx, VirtualMachineOption{
Name: device,
Value: fmt.Sprintf("%s:iso/%s,media=cdrom", storage.Name, isoName),
}, VirtualMachineOption{
Name: "boot",
Value: RemoveDeviceFromBootString(v.VirtualMachineConfig.Boot, device),
},
)

if err != nil {
return err
Expand All @@ -171,6 +183,59 @@ func (v *VirtualMachine) CloudInit(ctx context.Context, device, userdata, metada
return task.WaitFor(ctx, 2)
}

// MakeBootString takes a list of boot devices and returns a string of the format, "order=firstDevice;secondDevice"
func MakeBootString(devices ...string) string {
Copy link
Owner

Choose a reason for hiding this comment

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

probably no reason to export this, let's make it internal

var bootS string
for _, device := range devices {
bootS = bootS + ";" + device
}
bootS = strings.Trim(bootS, BootDeviceSeparator)
if bootS != "" {
bootS = BootOrderPfx + bootS
}
return bootS
}

// ParseBootString takes a boot string of the format, "order=firstDevice;secondDevice", and returns a slice of strings
// representing the boot devices in order.
func ParseBootString(bootS string) []string {
Copy link
Owner

Choose a reason for hiding this comment

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

probably no reason to export this, let's make it internal

trimmed := strings.TrimLeft(bootS, BootOrderPfx) // sata0;scsi1;ide0
return strings.Split(trimmed, BootDeviceSeparator)
}

// AppendDeviceToBootString adds a device to the end of the boot string.
func AppendDeviceToBootString(bootS, device string) string {
Copy link
Owner

Choose a reason for hiding this comment

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

probably no reason to export this, let's make it internal

devices := ParseBootString(bootS)
devices = append(devices, device)
return MakeBootString(devices...)
}

// remove element from string slice by value
func remove(s []string, r string) []string {
for i, v := range s {
if v == r {
return append(s[:i], s[i+1:]...)
}
}
return s
}

// RemoveDeviceFromBootString takes a boot string and returns a boot string minus a given device.
// If the given device is already not present in the boot order, this is basically a no-op.
func RemoveDeviceFromBootString(bootS, device string) string {
Copy link
Owner

Choose a reason for hiding this comment

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

probably no reason to export this, let's make it internal

devices := ParseBootString(bootS)
devices = remove(devices, device)
return MakeBootString(devices...)
}

// MoveDeviceToEndOfBootString moves a device to the end of the boot order.
// If the given device is not present in the boot order, this adds it at the end.
func MoveDeviceToEndOfBootString(bootS, device string) string {
Copy link
Owner

Choose a reason for hiding this comment

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

probably no reason to export this, let's make it internal

bootS = RemoveDeviceFromBootString(bootS, device)
bootS = AppendDeviceToBootString(bootS, device)
return bootS
}

func makeCloudInitISO(filename, userdata, metadata, vendordata, networkconfig string) (iso *os.File, err error) {
iso, err = os.Create(filepath.Join(os.TempDir(), filename))
if err != nil {
Expand Down Expand Up @@ -212,10 +277,12 @@ func makeCloudInitISO(filename, userdata, metadata, vendordata, networkconfig st
}
}

if err = fs.Finalize(iso9660.FinalizeOptions{
RockRidge: true,
VolumeIdentifier: volumeIdentifier,
}); err != nil {
if err = fs.Finalize(
iso9660.FinalizeOptions{
RockRidge: true,
VolumeIdentifier: volumeIdentifier,
},
); err != nil {
return nil, err
}

Expand All @@ -226,8 +293,10 @@ func makeCloudInitISO(filename, userdata, metadata, vendordata, networkconfig st
// send, recv, errors, closer, errors := vm.VNCWebSocket(vnc)
// for this to work you need to first set up a serial terminal on your vm https://pve.proxmox.com/wiki/Serial_Terminal
func (v *VirtualMachine) VNCWebSocket(vnc *VNC) (chan string, chan string, chan error, func() error, error) {
p := fmt.Sprintf("/nodes/%s/qemu/%d/vncwebsocket?port=%d&vncticket=%s",
v.Node, v.VMID, vnc.Port, url.QueryEscape(vnc.Ticket))
p := fmt.Sprintf(
"/nodes/%s/qemu/%d/vncwebsocket?port=%d&vncticket=%s",
v.Node, v.VMID, vnc.Port, url.QueryEscape(vnc.Ticket),
)

return v.client.VNCWebSocket(p, vnc)
}
Expand Down Expand Up @@ -260,7 +329,12 @@ func (v *VirtualMachine) Reset(ctx context.Context) (task *Task, err error) {

func (v *VirtualMachine) Shutdown(ctx context.Context) (task *Task, err error) {
var upid UPID
if err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/status/shutdown", v.Node, v.VMID), nil, &upid); err != nil {
if err = v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/status/shutdown", v.Node, v.VMID),
nil,
&upid,
); err != nil {
return nil, err
}

Expand All @@ -282,7 +356,12 @@ func (v *VirtualMachine) IsPaused() bool {

func (v *VirtualMachine) Pause(ctx context.Context) (task *Task, err error) {
var upid UPID
if err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/status/suspend", v.Node, v.VMID), nil, &upid); err != nil {
if err = v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/status/suspend", v.Node, v.VMID),
nil,
&upid,
); err != nil {
return nil, err
}

Expand All @@ -295,7 +374,12 @@ func (v *VirtualMachine) IsHibernated() bool {

func (v *VirtualMachine) Hibernate(ctx context.Context) (task *Task, err error) {
var upid UPID
if err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/status/suspend", v.Node, v.VMID), map[string]string{"todisk": "1"}, &upid); err != nil {
if err = v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/status/suspend", v.Node, v.VMID),
map[string]string{"todisk": "1"},
&upid,
); err != nil {
return nil, err
}

Expand All @@ -304,7 +388,12 @@ func (v *VirtualMachine) Hibernate(ctx context.Context) (task *Task, err error)

func (v *VirtualMachine) Resume(ctx context.Context) (task *Task, err error) {
var upid UPID
if err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/status/resume", v.Node, v.VMID), nil, &upid); err != nil {
if err = v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/status/resume", v.Node, v.VMID),
nil,
&upid,
); err != nil {
return nil, err
}

Expand All @@ -313,7 +402,12 @@ func (v *VirtualMachine) Resume(ctx context.Context) (task *Task, err error) {

func (v *VirtualMachine) Reboot(ctx context.Context) (task *Task, err error) {
var upid UPID
if err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/status/reboot", v.Node, v.VMID), nil, &upid); err != nil {
if err = v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/status/reboot", v.Node, v.VMID),
nil,
&upid,
); err != nil {
return nil, err
}

Expand Down Expand Up @@ -379,7 +473,11 @@ func (v *VirtualMachine) Migrate(
return NewTask(upid, v.client), nil
}

func (v *VirtualMachine) Clone(ctx context.Context, params *VirtualMachineCloneOptions) (newid int, task *Task, err error) {
func (v *VirtualMachine) Clone(ctx context.Context, params *VirtualMachineCloneOptions) (
newid int,
task *Task,
err error,
) {
var upid UPID

if params == nil {
Expand Down Expand Up @@ -407,10 +505,12 @@ func (v *VirtualMachine) Clone(ctx context.Context, params *VirtualMachineCloneO
}

func (v *VirtualMachine) ResizeDisk(ctx context.Context, disk, size string) (err error) {
err = v.client.Put(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/resize", v.Node, v.VMID), map[string]string{
"disk": disk,
"size": size,
}, nil)
err = v.client.Put(
ctx, fmt.Sprintf("/nodes/%s/qemu/%d/resize", v.Node, v.VMID), map[string]string{
"disk": disk,
"size": size,
}, nil,
)
if err != nil {
return
}
Expand All @@ -433,7 +533,10 @@ func (v *VirtualMachine) UnlinkDisk(ctx context.Context, diskID string, force bo
return NewTask(upid, v.client), nil
}

func (v *VirtualMachine) MoveDisk(ctx context.Context, disk string, params *VirtualMachineMoveDiskOptions) (task *Task, err error) {
func (v *VirtualMachine) MoveDisk(ctx context.Context, disk string, params *VirtualMachineMoveDiskOptions) (
task *Task,
err error,
) {
var upid UPID

if params == nil {
Expand Down Expand Up @@ -499,12 +602,14 @@ func (v *VirtualMachine) WaitForAgent(ctx context.Context, seconds int) error {

func (v *VirtualMachine) AgentExec(ctx context.Context, command, inputData string) (pid int, err error) {
tmpdata := map[string]interface{}{}
err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/agent/exec", v.Node, v.VMID),
err = v.client.Post(
ctx, fmt.Sprintf("/nodes/%s/qemu/%d/agent/exec", v.Node, v.VMID),
map[string]string{
"command": command,
"input-data": inputData,
},
&tmpdata)
&tmpdata,
)
pid = int(tmpdata["pid"].(float64))
return
}
Expand Down Expand Up @@ -557,10 +662,18 @@ func (v *VirtualMachine) AgentOsInfo(ctx context.Context) (info *AgentOsInfo, er
}

func (v *VirtualMachine) AgentSetUserPassword(ctx context.Context, password string, username string) error {
return v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/agent/set-user-password", v.Node, v.VMID), map[string]string{"password": password, "username": username}, nil)
}

func (v *VirtualMachine) FirewallOptionGet(ctx context.Context) (firewallOption *FirewallVirtualMachineOption, err error) {
return v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/agent/set-user-password", v.Node, v.VMID),
map[string]string{"password": password, "username": username},
nil,
)
}

func (v *VirtualMachine) FirewallOptionGet(ctx context.Context) (
firewallOption *FirewallVirtualMachineOption,
err error,
) {
err = v.client.Get(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/firewall/options", v.Node, v.VMID), firewallOption)
return
}
Expand Down Expand Up @@ -588,7 +701,12 @@ func (v *VirtualMachine) FirewallRulesDelete(ctx context.Context, rulePos int) e

func (v *VirtualMachine) NewSnapshot(ctx context.Context, name string) (task *Task, err error) {
var upid UPID
if err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/snapshot", v.Node, v.VMID), map[string]string{"snapname": name}, &upid); err != nil {
if err = v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/snapshot", v.Node, v.VMID),
map[string]string{"snapname": name},
&upid,
); err != nil {
return nil, err
}

Expand All @@ -602,7 +720,12 @@ func (v *VirtualMachine) Snapshots(ctx context.Context) (snapshots []*Snapshot,

func (v *VirtualMachine) SnapshotRollback(ctx context.Context, name string) (task *Task, err error) {
var upid UPID
if err = v.client.Post(ctx, fmt.Sprintf("/nodes/%s/qemu/%d/snapshot/%s/rollback", v.Node, v.VMID, name), nil, &upid); err != nil {
if err = v.client.Post(
ctx,
fmt.Sprintf("/nodes/%s/qemu/%d/snapshot/%s/rollback", v.Node, v.VMID, name),
nil,
&upid,
); err != nil {
return nil, err
}

Expand All @@ -611,7 +734,11 @@ func (v *VirtualMachine) SnapshotRollback(ctx context.Context, name string) (tas

// RRDData takes a timeframe enum and an optional consolidation function
// usage: vm.RRDData(HOURLY) or vm.RRDData(HOURLY, AVERAGE)
func (v *VirtualMachine) RRDData(ctx context.Context, timeframe Timeframe, consolidationFunction ...ConsolidationFunction) (rrddata []*RRDData, err error) {
func (v *VirtualMachine) RRDData(
ctx context.Context,
timeframe Timeframe,
consolidationFunction ...ConsolidationFunction,
) (rrddata []*RRDData, err error) {
u := url.URL{Path: fmt.Sprintf("/nodes/%s/qemu/%d/rrddata", v.Node, v.VMID)}

// consolidation functions are variadic because they're optional, putting everything into one string and sending that
Expand Down