Skip to content

Commit 8a4811d

Browse files
authored
Merge pull request #3846 from olamilekan000/add-progress-flag-to-start-cmd
add progress tracking flag to monitor VM startup state using cloud-init
2 parents c338ded + dcbab2f commit 8a4811d

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"
@@ -73,11 +74,14 @@ type HostAgent struct {
7374

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

7881
type options struct {
7982
guestAgentBinary string
8083
nerdctlArchive string // local path, not URL
84+
showProgress bool
8185
}
8286

8387
type Opt func(*options) error
@@ -96,6 +100,13 @@ func WithNerdctlArchive(s string) Opt {
96100
}
97101
}
98102

103+
func WithCloudInitProgress(enabled bool) Opt {
104+
return func(o *options) error {
105+
o.showProgress = enabled
106+
return nil
107+
}
108+
}
109+
99110
// New creates the HostAgent.
100111
//
101112
// stdout is for emitting JSON lines of Events.
@@ -227,6 +238,7 @@ func New(instName string, stdout io.Writer, signalCh chan os.Signal, opts ...Opt
227238
vSockPort: vSockPort,
228239
virtioPort: virtioPort,
229240
guestAgentAliveCh: make(chan struct{}),
241+
showProgress: o.showProgress,
230242
}
231243
return a, nil
232244
}
@@ -493,6 +505,18 @@ sudo chown -R "${USER}" /run/host-services`
493505
}
494506
if !*a.instConfig.Plain {
495507
go a.watchGuestAgentEvents(ctx)
508+
if a.showProgress {
509+
cloudInitDone := make(chan struct{})
510+
go func() {
511+
a.watchCloudInitProgress(ctx)
512+
close(cloudInitDone)
513+
}()
514+
515+
go func() {
516+
<-cloudInitDone
517+
logrus.Debug("Cloud-init monitoring completed, VM is fully ready")
518+
}()
519+
}
496520
}
497521
if err := a.waitForRequirements("optional", a.optionalRequirements()); err != nil {
498522
errs = append(errs, err)
@@ -790,6 +814,141 @@ func forwardSSH(ctx context.Context, sshConfig *ssh.SSHConfig, port int, local,
790814
return nil
791815
}
792816

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