Skip to content

Commit dcbab2f

Browse files
committed
add progress tracking flag to monitor VM startup state using cloud-init
Signed-off-by: olalekan odukoya <[email protected]>
1 parent 5efb04b commit dcbab2f

File tree

9 files changed

+227
-10
lines changed

9 files changed

+227
-10
lines changed

cmd/limactl/clone.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ func cloneAction(cmd *cobra.Command, args []string) error {
109109
if err != nil {
110110
return err
111111
}
112-
return instance.Start(ctx, newInst, "", false)
112+
return instance.Start(ctx, newInst, "", false, false)
113113
}
114114

115115
func cloneBashComplete(cmd *cobra.Command, _ []string, _ string) ([]string, cobra.ShellCompDirective) {

cmd/limactl/edit.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ func editAction(cmd *cobra.Command, args []string) error {
160160
if err != nil {
161161
return err
162162
}
163-
return instance.Start(ctx, inst, "", false)
163+
return instance.Start(ctx, inst, "", false, false)
164164
}
165165

166166
func askWhetherToStart() (bool, error) {

cmd/limactl/hostagent.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ func newHostagentCommand() *cobra.Command {
3535
hostagentCommand.Flags().Bool("run-gui", false, "Run GUI synchronously within hostagent")
3636
hostagentCommand.Flags().String("guestagent", "", "Local file path (not URL) of lima-guestagent.OS-ARCH[.gz]")
3737
hostagentCommand.Flags().String("nerdctl-archive", "", "Local file path (not URL) of nerdctl-full-VERSION-GOOS-GOARCH.tar.gz")
38+
hostagentCommand.Flags().Bool("progress", false, "Show provision script progress by monitoring cloud-init logs")
3839
return hostagentCommand
3940
}
4041

@@ -94,6 +95,13 @@ func hostagentAction(cmd *cobra.Command, args []string) error {
9495
if nerdctlArchive != "" {
9596
opts = append(opts, hostagent.WithNerdctlArchive(nerdctlArchive))
9697
}
98+
showProgress, err := cmd.Flags().GetBool("progress")
99+
if err != nil {
100+
return err
101+
}
102+
if showProgress {
103+
opts = append(opts, hostagent.WithCloudInitProgress(showProgress))
104+
}
97105
ha, err := hostagent.New(instName, stdout, signalCh, opts...)
98106
if err != nil {
99107
return err

cmd/limactl/shell.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ func shellAction(cmd *cobra.Command, args []string) error {
101101
return err
102102
}
103103

104-
err = instance.Start(ctx, inst, "", false)
104+
err = instance.Start(ctx, inst, "", false, false)
105105
if err != nil {
106106
return err
107107
}

cmd/limactl/start.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ See the examples in 'limactl create --help'.
9999
startCommand.Flags().Bool("foreground", false, "Run the hostagent in the foreground")
100100
}
101101
startCommand.Flags().Duration("timeout", instance.DefaultWatchHostAgentEventsTimeout, "Duration to wait for the instance to be running before timing out")
102+
startCommand.Flags().Bool("progress", false, "Show provision script progress by tailing cloud-init logs")
102103
return startCommand
103104
}
104105

@@ -493,7 +494,12 @@ func startAction(cmd *cobra.Command, args []string) error {
493494
ctx = instance.WithWatchHostAgentTimeout(ctx, timeout)
494495
}
495496

496-
return instance.Start(ctx, inst, "", launchHostAgentForeground)
497+
progress, err := cmd.Flags().GetBool("progress")
498+
if err != nil {
499+
return err
500+
}
501+
502+
return instance.Start(ctx, inst, "", launchHostAgentForeground, progress)
497503
}
498504

499505
func createBashComplete(cmd *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) {

pkg/hostagent/events/events.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ type Status struct {
1717
Errors []string `json:"errors,omitempty"`
1818

1919
SSHLocalPort int `json:"sshLocalPort,omitempty"`
20+
21+
// Cloud-init progress information
22+
CloudInitProgress *CloudInitProgress `json:"cloudInitProgress,omitempty"`
23+
}
24+
25+
type CloudInitProgress struct {
26+
// Current log line from cloud-init
27+
LogLine string `json:"logLine,omitempty"`
28+
// Whether cloud-init has completed
29+
Completed bool `json:"completed,omitempty"`
30+
// Whether cloud-init monitoring is active
31+
Active bool `json:"active,omitempty"`
2032
}
2133

2234
type Event struct {

pkg/hostagent/hostagent.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package hostagent
55

66
import (
7+
"bufio"
78
"bytes"
89
"context"
910
"encoding/json"
@@ -72,11 +73,14 @@ type HostAgent struct {
7273

7374
guestAgentAliveCh chan struct{} // closed on establishing the connection
7475
guestAgentAliveChOnce sync.Once
76+
77+
showProgress bool // whether to show cloud-init progress
7578
}
7679

7780
type options struct {
7881
guestAgentBinary string
7982
nerdctlArchive string // local path, not URL
83+
showProgress bool
8084
}
8185

8286
type Opt func(*options) error
@@ -95,6 +99,13 @@ func WithNerdctlArchive(s string) Opt {
9599
}
96100
}
97101

102+
func WithCloudInitProgress(enabled bool) Opt {
103+
return func(o *options) error {
104+
o.showProgress = enabled
105+
return nil
106+
}
107+
}
108+
98109
// New creates the HostAgent.
99110
//
100111
// stdout is for emitting JSON lines of Events.
@@ -214,6 +225,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
214225
vSockPort: vSockPort,
215226
virtioPort: virtioPort,
216227
guestAgentAliveCh: make(chan struct{}),
228+
showProgress: o.showProgress,
217229
}
218230
return a, nil
219231
}
@@ -480,6 +492,18 @@ sudo chown -R "${USER}" /run/host-services`
480492
}
481493
if !*a.instConfig.Plain {
482494
go a.watchGuestAgentEvents(ctx)
495+
if a.showProgress {
496+
cloudInitDone := make(chan struct{})
497+
go func() {
498+
a.watchCloudInitProgress(ctx)
499+
close(cloudInitDone)
500+
}()
501+
502+
go func() {
503+
<-cloudInitDone
504+
logrus.Debug("Cloud-init monitoring completed, VM is fully ready")
505+
}()
506+
}
483507
}
484508
if err := a.waitForRequirements("optional", a.optionalRequirements()); err != nil {
485509
errs = append(errs, err)
@@ -777,6 +801,141 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
777801
return nil
778802
}
779803

804+
func (a *HostAgent) watchCloudInitProgress(ctx context.Context) {
805+
logrus.Debug("Starting cloud-init progress monitoring")
806+
807+
a.emitEvent(ctx, events.Event{
808+
Status: events.Status{
809+
SSHLocalPort: a.sshLocalPort,
810+
CloudInitProgress: &events.CloudInitProgress{
811+
Active: true,
812+
},
813+
},
814+
})
815+
816+
maxRetries := 30
817+
retryDelay := time.Second
818+
var sshReady bool
819+
820+
for i := 0; i < maxRetries && !sshReady; i++ {
821+
if i > 0 {
822+
time.Sleep(retryDelay)
823+
}
824+
825+
// Test SSH connectivity
826+
args := a.sshConfig.Args()
827+
args = append(args,
828+
"-p", strconv.Itoa(a.sshLocalPort),
829+
"127.0.0.1",
830+
"echo 'SSH Ready'",
831+
)
832+
833+
cmd := exec.CommandContext(ctx, a.sshConfig.Binary(), args...)
834+
if err := cmd.Run(); err == nil {
835+
sshReady = true
836+
logrus.Debug("SSH ready for cloud-init monitoring")
837+
}
838+
}
839+
840+
if !sshReady {
841+
logrus.Warn("SSH not ready for cloud-init monitoring, proceeding anyway")
842+
}
843+
844+
args := a.sshConfig.Args()
845+
args = append(args,
846+
"-p", strconv.Itoa(a.sshLocalPort),
847+
"127.0.0.1",
848+
"sudo", "tail", "-n", "+1", "-f", "/var/log/cloud-init-output.log",
849+
)
850+
851+
cmd := exec.CommandContext(ctx, a.sshConfig.Binary(), args...)
852+
stdout, err := cmd.StdoutPipe()
853+
if err != nil {
854+
logrus.WithError(err).Warn("Failed to create stdout pipe for cloud-init monitoring")
855+
return
856+
}
857+
858+
if err := cmd.Start(); err != nil {
859+
logrus.WithError(err).Warn("Failed to start cloud-init monitoring command")
860+
return
861+
}
862+
863+
scanner := bufio.NewScanner(stdout)
864+
cloudInitFinished := false
865+
866+
for scanner.Scan() {
867+
line := scanner.Text()
868+
if strings.TrimSpace(line) == "" {
869+
continue
870+
}
871+
872+
if strings.Contains(line, "Cloud-init") && strings.Contains(line, "finished") {
873+
cloudInitFinished = true
874+
}
875+
876+
a.emitEvent(ctx, events.Event{
877+
Status: events.Status{
878+
SSHLocalPort: a.sshLocalPort,
879+
CloudInitProgress: &events.CloudInitProgress{
880+
Active: !cloudInitFinished,
881+
LogLine: line,
882+
Completed: cloudInitFinished,
883+
},
884+
},
885+
})
886+
}
887+
888+
if err := cmd.Wait(); err != nil {
889+
logrus.WithError(err).Debug("SSH command finished (expected when cloud-init completes)")
890+
}
891+
892+
if !cloudInitFinished {
893+
logrus.Debug("Connection dropped, checking for any remaining cloud-init logs")
894+
895+
finalArgs := a.sshConfig.Args()
896+
finalArgs = append(finalArgs,
897+
"-p", strconv.Itoa(a.sshLocalPort),
898+
"127.0.0.1",
899+
"sudo", "tail", "-n", "20", "/var/log/cloud-init-output.log",
900+
)
901+
902+
finalCmd := exec.CommandContext(ctx, a.sshConfig.Binary(), finalArgs...)
903+
if finalOutput, err := finalCmd.Output(); err == nil {
904+
lines := strings.Split(string(finalOutput), "\n")
905+
for _, line := range lines {
906+
if strings.TrimSpace(line) != "" {
907+
if strings.Contains(line, "Cloud-init") && strings.Contains(line, "finished") {
908+
cloudInitFinished = true
909+
}
910+
911+
a.emitEvent(ctx, events.Event{
912+
Status: events.Status{
913+
SSHLocalPort: a.sshLocalPort,
914+
CloudInitProgress: &events.CloudInitProgress{
915+
Active: !cloudInitFinished,
916+
LogLine: line,
917+
Completed: cloudInitFinished,
918+
},
919+
},
920+
})
921+
}
922+
}
923+
}
924+
}
925+
926+
a.emitEvent(ctx, events.Event{
927+
Status: events.Status{
928+
SSHLocalPort: a.sshLocalPort,
929+
CloudInitProgress: &events.CloudInitProgress{
930+
Active: false,
931+
Completed: true,
932+
},
933+
},
934+
})
935+
936+
logrus.Debug("Cloud-init progress monitoring completed")
937+
}
938+
780939
func copyToHost(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local, remote string) error {
781940
args := sshConfig.Args()
782941
args = append(args,

pkg/instance/restart.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import (
1212
"github.com/lima-vm/lima/v2/pkg/store"
1313
)
1414

15-
const launchHostAgentForeground = false
15+
const (
16+
launchHostAgentForeground = false
17+
showProgress = false
18+
)
1619

1720
func Restart(ctx context.Context, inst *store.Instance) error {
1821
if err := StopGracefully(ctx, inst, true); err != nil {
@@ -23,7 +26,7 @@ func Restart(ctx context.Context, inst *store.Instance) error {
2326
return err
2427
}
2528

26-
if err := Start(ctx, inst, "", launchHostAgentForeground); err != nil {
29+
if err := Start(ctx, inst, "", launchHostAgentForeground, showProgress); err != nil {
2730
return err
2831
}
2932

@@ -38,7 +41,7 @@ func RestartForcibly(ctx context.Context, inst *store.Instance) error {
3841
return err
3942
}
4043

41-
if err := Start(ctx, inst, "", launchHostAgentForeground); err != nil {
44+
if err := Start(ctx, inst, "", launchHostAgentForeground, showProgress); err != nil {
4245
return err
4346
}
4447

0 commit comments

Comments
 (0)