44package hostagent
55
66import (
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
7881type options struct {
7982 guestAgentBinary string
8083 nerdctlArchive string // local path, not URL
84+ showProgress bool
8185}
8286
8387type 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+
793952func copyToHost (ctx context.Context , sshConfig * ssh.SSHConfig , port int , local , remote string ) error {
794953 args := sshConfig .Args ()
795954 args = append (args ,
0 commit comments