File: step-wait-cloud-init.go

package info (click to toggle)
packer 1.6.6%2Bds2-1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 33,156 kB
  • sloc: sh: 1,154; python: 619; makefile: 251; ruby: 205; xml: 97
file content (116 lines) | stat: -rw-r--r-- 3,028 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
package yandexexport

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/hashicorp/packer/builder/yandex"
	"github.com/hashicorp/packer/packer-plugin-sdk/multistep"
	packersdk "github.com/hashicorp/packer/packer-plugin-sdk/packer"
	"github.com/hashicorp/packer/packer-plugin-sdk/retry"
)

type StepWaitCloudInitScript struct {
	Tries int
}

type cloudInitStatus struct {
	V1 struct {
		Errors []interface{}
	}
}

type cloudInitError struct {
	Err error
}

func (e *cloudInitError) Error() string {
	return e.Err.Error()
}

// Run reads the instance metadata and looks for the log entry
// indicating the cloud-init script finished.
func (s *StepWaitCloudInitScript) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
	ui := state.Get("ui").(packersdk.Ui)
	comm := state.Get("communicator").(packersdk.Communicator)

	ui.Say("Waiting for any running cloud-init script to finish...")

	ctxWithCancel, cancelCtx := context.WithCancel(ctx)

	defer cancelCtx()

	go func() {
		cmd := &packersdk.RemoteCmd{
			Command: "tail -f /var/log/cloud-init-output.log",
		}

		err := cmd.RunWithUi(ctxWithCancel, comm, ui)
		if err != nil && !errors.Is(err, context.Canceled) {
			ui.Error(err.Error())
			return
		}
		ui.Message("Cloud-init output closed")
	}()

	// periodically show progress by sending SIGUSR1 to `qemu-img` process
	go func() {
		cmd := &packersdk.RemoteCmd{
			Command: "until pid=$(pidof qemu-img) ; do sleep 1 ; done ; " +
				"while true ; do sudo kill -s SIGUSR1 ${pid}; sleep 10 ; done",
		}

		err := cmd.RunWithUi(ctxWithCancel, comm, ui)
		if err != nil && !errors.Is(err, context.Canceled) {
			ui.Error("qemu-img signal sender error: " + err.Error())
			return
		}
	}()

	// Keep checking the serial port output to see if the cloud-init script is done.
	retryConfig := &retry.Config{
		ShouldRetry: func(e error) bool {
			switch e.(type) {
			case *cloudInitError:
				return false
			}
			return true
		},
		Tries:      s.Tries,
		RetryDelay: (&retry.Backoff{InitialBackoff: 10 * time.Second, MaxBackoff: 60 * time.Second, Multiplier: 2}).Linear,
	}

	err := retryConfig.Run(ctx, func(ctx context.Context) error {
		buff := bytes.Buffer{}
		err := comm.Download("/var/run/cloud-init/result.json", &buff)
		if err != nil {
			err := fmt.Errorf("Waiting cloud-init script status: %s", err)
			return err
		}
		result := &cloudInitStatus{}
		err = json.Unmarshal(buff.Bytes(), result)
		if err != nil {
			err := fmt.Errorf("Failed parse result: %s", err)
			return &cloudInitError{Err: err}
		}
		if len(result.V1.Errors) != 0 {
			err := fmt.Errorf("Result: %v", result.V1.Errors)
			return &cloudInitError{Err: err}
		}
		return nil
	})

	if err != nil {
		err := fmt.Errorf("Error waiting for cloud-init script to finish: %s", err)
		return yandex.StepHaltWithError(state, err)
	}
	ui.Say("Cloud-init script has finished running.")
	return multistep.ActionContinue
}

// Cleanup.
func (s *StepWaitCloudInitScript) Cleanup(state multistep.StateBag) {}