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