@@ -40,6 +40,7 @@ type DomainResourceModel struct {
4040 Running types.Bool `tfsdk:"running"`
4141 Autostart types.Bool `tfsdk:"autostart"`
4242 Create types.Object `tfsdk:"create"`
43+ Update types.Object `tfsdk:"update"`
4344 Destroy types.Object `tfsdk:"destroy"`
4445}
4546
@@ -67,21 +68,27 @@ type DomainCreateModel struct {
6768 ResetNVRAM types.Bool `tfsdk:"reset_nvram"`
6869}
6970
71+ // DomainUpdateModel describes domain update stop behavior.
72+ type DomainUpdateModel struct {
73+ Shutdown types.Object `tfsdk:"shutdown"`
74+ }
75+
7076// DomainDestroyModel describes domain shutdown behavior
7177type DomainDestroyModel struct {
7278 Graceful types.Bool `tfsdk:"graceful"`
7379 Shutdown types.Object `tfsdk:"shutdown"`
7480}
7581
76- // DomainDestroyShutdownModel describes optional shutdown wait behavior.
77- type DomainDestroyShutdownModel struct {
82+ // DomainShutdownModel describes optional shutdown wait behavior.
83+ type DomainShutdownModel struct {
7884 Timeout types.Int64 `tfsdk:"timeout"`
7985}
8086
81- type domainDestroyOptions struct {
87+ type domainStopOptions struct {
8288 Flags golibvirt.DomainDestroyFlagsValues
8389 ShutdownEnabled bool
8490 ShutdownTimeout time.Duration
91+ ForceOnTimeout bool
8592}
8693
8794type domainPlanData struct {
@@ -458,9 +465,59 @@ func domainDestroyFlagsFromDestroy(ctx context.Context, destroyVal types.Object)
458465 return flags , nil
459466}
460467
461- func domainDestroyOptionsFromDestroy (ctx context.Context , destroyVal types.Object ) (domainDestroyOptions , diag.Diagnostics ) {
468+ func domainShutdownTimeoutFromObject (ctx context.Context , shutdownVal types.Object , defaultTimeout time.Duration ) (time.Duration , diag.Diagnostics ) {
469+ if shutdownVal .IsNull () || shutdownVal .IsUnknown () {
470+ return defaultTimeout , nil
471+ }
472+
473+ var shutdownModel DomainShutdownModel
474+ diags := shutdownVal .As (ctx , & shutdownModel , basetypes.ObjectAsOptions {})
475+ if diags .HasError () {
476+ return defaultTimeout , diags
477+ }
478+
479+ if ! shutdownModel .Timeout .IsNull () && ! shutdownModel .Timeout .IsUnknown () {
480+ timeout := shutdownModel .Timeout .ValueInt64 ()
481+ if timeout > 0 {
482+ return time .Duration (timeout ) * time .Second , nil
483+ }
484+ }
485+
486+ return defaultTimeout , nil
487+ }
488+
489+ func domainUpdateOptionsFromUpdate (ctx context.Context , updateVal types.Object ) (domainStopOptions , diag.Diagnostics ) {
490+ options := domainStopOptions {
491+ ShutdownEnabled : true ,
492+ ShutdownTimeout : 30 * time .Second ,
493+ ForceOnTimeout : true ,
494+ }
495+
496+ if updateVal .IsNull () || updateVal .IsUnknown () {
497+ return options , nil
498+ }
499+
500+ var updateModel DomainUpdateModel
501+ diags := updateVal .As (ctx , & updateModel , basetypes.ObjectAsOptions {})
502+ if diags .HasError () {
503+ return options , diags
504+ }
505+
506+ if ! updateModel .Shutdown .IsNull () && ! updateModel .Shutdown .IsUnknown () {
507+ timeout , timeoutDiags := domainShutdownTimeoutFromObject (ctx , updateModel .Shutdown , options .ShutdownTimeout )
508+ diags .Append (timeoutDiags ... )
509+ if diags .HasError () {
510+ return options , diags
511+ }
512+ options .ShutdownTimeout = timeout
513+ }
514+
515+ return options , diags
516+ }
517+
518+ func domainDestroyOptionsFromDestroy (ctx context.Context , destroyVal types.Object ) (domainStopOptions , diag.Diagnostics ) {
462519 flags , diags := domainDestroyFlagsFromDestroy (ctx , destroyVal )
463- options := domainDestroyOptions {
520+ options := domainStopOptions {
464521 Flags : flags ,
465522 }
466523 if diags .HasError () || destroyVal .IsNull () || destroyVal .IsUnknown () {
@@ -476,19 +533,12 @@ func domainDestroyOptionsFromDestroy(ctx context.Context, destroyVal types.Objec
476533 if ! destroyModel .Shutdown .IsNull () && ! destroyModel .Shutdown .IsUnknown () {
477534 options .ShutdownEnabled = true
478535 options .ShutdownTimeout = 30 * time .Second
479-
480- var shutdownModel DomainDestroyShutdownModel
481- diags = destroyModel .Shutdown .As (ctx , & shutdownModel , basetypes.ObjectAsOptions {})
536+ timeout , timeoutDiags := domainShutdownTimeoutFromObject (ctx , destroyModel .Shutdown , options .ShutdownTimeout )
537+ diags .Append (timeoutDiags ... )
482538 if diags .HasError () {
483539 return options , diags
484540 }
485-
486- if ! shutdownModel .Timeout .IsNull () && ! shutdownModel .Timeout .IsUnknown () {
487- timeout := shutdownModel .Timeout .ValueInt64 ()
488- if timeout > 0 {
489- options .ShutdownTimeout = time .Duration (timeout ) * time .Second
490- }
491- }
541+ options .ShutdownTimeout = timeout
492542 }
493543
494544 return options , nil
@@ -523,6 +573,22 @@ func (r *DomainResource) Schema(ctx context.Context, req resource.SchemaRequest,
523573 "reset_nvram" : schema.BoolAttribute {Optional : true },
524574 },
525575 },
576+ "update" : schema.SingleNestedAttribute {
577+ Description : "Update behavior when Terraform must stop the domain before redefining it." ,
578+ Optional : true ,
579+ Attributes : map [string ]schema.Attribute {
580+ "shutdown" : schema.SingleNestedAttribute {
581+ Description : "Experimental: request a guest shutdown and wait for shutoff before forcing a stop during update. Subject to change in future releases." ,
582+ Optional : true ,
583+ Attributes : map [string ]schema.Attribute {
584+ "timeout" : schema.Int64Attribute {
585+ Description : "Experimental: seconds to wait for guest shutdown before forcing a stop during update. Defaults to 30." ,
586+ Optional : true ,
587+ },
588+ },
589+ },
590+ },
591+ },
526592 "destroy" : schema.SingleNestedAttribute {
527593 Description : "Destroy behavior when Terraform removes the domain." ,
528594 Optional : true ,
@@ -574,6 +640,41 @@ func (r *DomainResource) Configure(ctx context.Context, req resource.ConfigureRe
574640 r .client = client
575641}
576642
643+ func (r * DomainResource ) stopDomainIfRunning (domain golibvirt.Domain , options domainStopOptions ) (bool , error ) {
644+ domainState , _ , err := r .client .Libvirt ().DomainGetState (domain , 0 )
645+ if err != nil {
646+ return false , fmt .Errorf ("check domain state: %w" , err )
647+ }
648+
649+ if uint32 (domainState ) != uint32 (golibvirt .DomainRunning ) {
650+ return false , nil
651+ }
652+
653+ if options .ShutdownEnabled {
654+ if err := r .client .Libvirt ().DomainShutdown (domain ); err != nil {
655+ return false , fmt .Errorf ("request guest shutdown: %w" , err )
656+ }
657+
658+ if err := waitForDomainState (r .client , domain , uint32 (golibvirt .DomainShutoff ), options .ShutdownTimeout ); err != nil {
659+ if ! options .ForceOnTimeout {
660+ return true , fmt .Errorf ("wait for shutdown: %w" , err )
661+ }
662+
663+ if destroyErr := r .client .Libvirt ().DomainDestroyFlags (domain , options .Flags ); destroyErr != nil {
664+ return false , fmt .Errorf ("force stop after shutdown timeout: %w" , destroyErr )
665+ }
666+ }
667+
668+ return false , nil
669+ }
670+
671+ if err := r .client .Libvirt ().DomainDestroyFlags (domain , options .Flags ); err != nil {
672+ return false , fmt .Errorf ("force stop running domain: %w" , err )
673+ }
674+
675+ return false , nil
676+ }
677+
577678// Create creates a new domain
578679func (r * DomainResource ) Create (ctx context.Context , req resource.CreateRequest , resp * resource.CreateResponse ) {
579680 var plan DomainResourceModel
@@ -709,6 +810,7 @@ func (r *DomainResource) Create(ctx context.Context, req resource.CreateRequest,
709810 Running : plan .Running ,
710811 Autostart : plan .Autostart ,
711812 Create : plan .Create ,
813+ Update : plan .Update ,
712814 Destroy : plan .Destroy ,
713815 }
714816
@@ -882,33 +984,18 @@ func (r *DomainResource) Update(ctx context.Context, req resource.UpdateRequest,
882984 return
883985 }
884986
885- domainState , _ , err := r .client .Libvirt ().DomainGetState (existingDomain , 0 )
886- if err != nil {
887- resp .Diagnostics .AddError (
888- "Failed to Get Domain State" ,
889- "Failed to check if domain is running: " + err .Error (),
890- )
987+ updateOptions , updateDiags := domainUpdateOptionsFromUpdate (ctx , plan .Update )
988+ resp .Diagnostics .Append (updateDiags ... )
989+ if resp .Diagnostics .HasError () {
891990 return
892991 }
893992
894- if uint32 (domainState ) == uint32 (golibvirt .DomainRunning ) {
895- if err := r .client .Libvirt ().DomainShutdown (existingDomain ); err != nil {
896- resp .Diagnostics .AddError (
897- "Failed to Shutdown Domain" ,
898- "Domain must be stopped before updating. Failed to shutdown: " + err .Error (),
899- )
900- return
901- }
902-
903- if err := waitForDomainState (r .client , existingDomain , uint32 (golibvirt .DomainShutoff ), 5 * time .Second ); err != nil {
904- if err := r .client .Libvirt ().DomainDestroy (existingDomain ); err != nil {
905- resp .Diagnostics .AddError (
906- "Failed to Stop Domain" ,
907- "Domain must be stopped before updating. Failed to force stop: " + err .Error (),
908- )
909- return
910- }
911- }
993+ if _ , err := r .stopDomainIfRunning (existingDomain , updateOptions ); err != nil {
994+ resp .Diagnostics .AddError (
995+ "Failed to Stop Domain" ,
996+ "Domain must be stopped before updating: " + err .Error (),
997+ )
998+ return
912999 }
9131000
9141001 domainXML , err := generated .DomainToXML (ctx , & planData .SanitizedModel )
@@ -1036,6 +1123,7 @@ func (r *DomainResource) Update(ctx context.Context, req resource.UpdateRequest,
10361123 Running : plan .Running ,
10371124 Autostart : plan .Autostart ,
10381125 Create : plan .Create ,
1126+ Update : plan .Update ,
10391127 Destroy : plan .Destroy ,
10401128 }
10411129
@@ -1089,48 +1177,23 @@ func (r *DomainResource) Delete(ctx context.Context, req resource.DeleteRequest,
10891177 return
10901178 }
10911179
1092- // Destroy (stop) the domain if it's running
1093- domainState , _ , err := r .client .Libvirt ().DomainGetState (domain , 0 )
1180+ timedOut , err := r .stopDomainIfRunning (domain , destroyOptions )
10941181 if err != nil {
1182+ if timedOut {
1183+ resp .Diagnostics .AddError (
1184+ "Timeout Waiting for Domain Shutdown" ,
1185+ fmt .Sprintf ("Domain did not reach shutoff state within %s: %s" , destroyOptions .ShutdownTimeout , err ),
1186+ )
1187+ return
1188+ }
1189+
10951190 resp .Diagnostics .AddError (
1096- "Failed to Get Domain State " ,
1097- "Failed to check domain state : " + err .Error (),
1191+ "Failed to Destroy Domain" ,
1192+ "Failed to stop running domain : " + err .Error (),
10981193 )
10991194 return
11001195 }
11011196
1102- // DomainState values: 0=nostate, 1=running, 2=blocked, 3=paused, 4=shutdown, 5=shutoff, 6=crashed, 7=pmsuspended
1103- if uint32 (domainState ) == uint32 (golibvirt .DomainRunning ) {
1104- if destroyOptions .ShutdownEnabled {
1105- err = r .client .Libvirt ().DomainShutdown (domain )
1106- if err != nil {
1107- resp .Diagnostics .AddError (
1108- "Failed to Shutdown Domain" ,
1109- "Failed to request guest shutdown: " + err .Error (),
1110- )
1111- return
1112- }
1113-
1114- err = waitForDomainState (r .client , domain , uint32 (golibvirt .DomainShutoff ), destroyOptions .ShutdownTimeout )
1115- if err != nil {
1116- resp .Diagnostics .AddError (
1117- "Timeout Waiting for Domain Shutdown" ,
1118- fmt .Sprintf ("Domain did not reach shutoff state within %s: %s" , destroyOptions .ShutdownTimeout , err ),
1119- )
1120- return
1121- }
1122- } else {
1123- err = r .client .Libvirt ().DomainDestroyFlags (domain , destroyOptions .Flags )
1124- if err != nil {
1125- resp .Diagnostics .AddError (
1126- "Failed to Destroy Domain" ,
1127- "Failed to stop running domain: " + err .Error (),
1128- )
1129- return
1130- }
1131- }
1132- }
1133-
11341197 libvirtVersion , err := r .client .Libvirt ().ConnectGetLibVersion ()
11351198 if err != nil {
11361199 resp .Diagnostics .AddError (
0 commit comments