Skip to content

Commit 1d4e957

Browse files
committed
Implement hypercore_vm_replication resource
1 parent 1e3299f commit 1d4e957

File tree

4 files changed

+407
-31
lines changed

4 files changed

+407
-31
lines changed
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package provider
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/path"
11+
"github.com/hashicorp/terraform-plugin-framework/resource"
12+
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
13+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
14+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
15+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
16+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
17+
"github.com/hashicorp/terraform-plugin-framework/types"
18+
"github.com/hashicorp/terraform-plugin-log/tflog"
19+
"github.com/hashicorp/terraform-provider-hypercore/internal/utils"
20+
)
21+
22+
// Ensure provider defined types fully satisfy framework interfaces.
23+
var _ resource.Resource = &HypercoreVMReplicationResource{}
24+
var _ resource.ResourceWithImportState = &HypercoreVMReplicationResource{}
25+
26+
func NewHypercoreVMReplicationResource() resource.Resource {
27+
return &HypercoreVMReplicationResource{}
28+
}
29+
30+
// HypercoreVMReplicationResource defines the resource implementation.
31+
type HypercoreVMReplicationResource struct {
32+
client *utils.RestClient
33+
}
34+
35+
// HypercoreVMReplicationResourceModel describes the resource data model.
36+
type HypercoreVMReplicationResourceModel struct {
37+
Id types.String `tfsdk:"id"`
38+
VmUUID types.String `tfsdk:"vm_uuid"`
39+
Label types.String `tfsdk:"label"`
40+
ConnectionUUID types.String `tfsdk:"connection_uuid"`
41+
Enable types.Bool `tfsdk:"enable"`
42+
TargetVmUUID types.String `tfsdk:"target_vm_uuid"`
43+
}
44+
45+
func (r *HypercoreVMReplicationResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
46+
resp.TypeName = req.ProviderTypeName + "_vm_replication"
47+
}
48+
49+
func (r *HypercoreVMReplicationResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
50+
resp.Schema = schema.Schema{
51+
// This description is used by the documentation generator and the language server.
52+
MarkdownDescription: "Hypercore VM replication resource to manage VM boot devices' order",
53+
Attributes: map[string]schema.Attribute{
54+
"id": schema.StringAttribute{
55+
Computed: true,
56+
MarkdownDescription: "Replication identifier",
57+
PlanModifiers: []planmodifier.String{
58+
stringplanmodifier.UseStateForUnknown(),
59+
},
60+
},
61+
"target_vm_uuid": schema.StringAttribute{
62+
Computed: true,
63+
MarkdownDescription: "Remote target VM UUID",
64+
PlanModifiers: []planmodifier.String{
65+
stringplanmodifier.UseStateForUnknown(),
66+
},
67+
},
68+
"vm_uuid": schema.StringAttribute{
69+
MarkdownDescription: "VM UUID of which we want to make a replication",
70+
Required: true,
71+
},
72+
"label": schema.StringAttribute{
73+
MarkdownDescription: "Human-readable label describing the replication purpose",
74+
Computed: true,
75+
Optional: true,
76+
Default: stringdefault.StaticString(""),
77+
},
78+
"connection_uuid": schema.StringAttribute{
79+
MarkdownDescription: "Remote connection UUID",
80+
Computed: true,
81+
Optional: true,
82+
},
83+
"enable": schema.BoolAttribute{
84+
MarkdownDescription: "Enable or disable replication",
85+
Computed: true,
86+
Optional: true,
87+
Default: booldefault.StaticBool(true),
88+
},
89+
},
90+
}
91+
}
92+
93+
func (r *HypercoreVMReplicationResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
94+
tflog.Info(ctx, "TTRT HypercoreVMReplicationResource CONFIGURE")
95+
// Prevent padisk if the provider has not been configured.
96+
if req.ProviderData == nil {
97+
return
98+
}
99+
100+
restClient, ok := req.ProviderData.(*utils.RestClient)
101+
102+
if !ok {
103+
resp.Diagnostics.AddError(
104+
"Unexpected Resource Configure Type",
105+
fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData),
106+
)
107+
108+
return
109+
}
110+
111+
r.client = restClient
112+
}
113+
114+
func (r *HypercoreVMReplicationResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
115+
tflog.Info(ctx, "TTRT HypercoreVMReplicationResource CREATE")
116+
var data HypercoreVMReplicationResourceModel
117+
118+
// Read Terraform plan data into the model
119+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
120+
121+
if r.client == nil {
122+
resp.Diagnostics.AddError(
123+
"Unconfigured HTTP Client",
124+
"Expected configured HTTP client. Please report this issue to the provider developers.",
125+
)
126+
return
127+
}
128+
if resp.Diagnostics.HasError() {
129+
return
130+
}
131+
132+
restClient := *r.client
133+
vmUUID := data.VmUUID.ValueString() // is required
134+
connectionUUID := data.ConnectionUUID.ValueString() // should be required
135+
label := data.Label.ValueString() // default empty string ""
136+
enable := data.Enable.ValueBool() // default true
137+
138+
if connectionUUID == "" {
139+
resp.Diagnostics.AddError(
140+
"Missing connection_uuid",
141+
"Parameter 'connection_uuid' is required for creating a VM replication",
142+
)
143+
}
144+
145+
tflog.Info(ctx, fmt.Sprintf("TTRT Create: vm_uuid=%s, connection_uuid=%s, label=%s, enable=%t", vmUUID, connectionUUID, label, enable))
146+
147+
replicationUUID, replication := utils.CreateVMReplication(restClient, vmUUID, connectionUUID, label, enable, ctx)
148+
149+
tflog.Info(ctx, fmt.Sprintf("TTRT Created: vm_uuid=%s, connection_uuid=%s, label=%s, enable=%t, replication=%v", vmUUID, connectionUUID, label, enable, replication))
150+
151+
// TODO: Check if HC3 matches TF
152+
// save into the Terraform state.
153+
data.Id = types.StringValue(replicationUUID)
154+
data.TargetVmUUID = types.StringValue(utils.AnyToString(replication["targetDomainUUID"]))
155+
156+
// Write logs using the tflog package
157+
// Documentation: https://terraform.io/plugin/log
158+
tflog.Trace(ctx, "Created a replication")
159+
160+
// Save data into Terraform state
161+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
162+
}
163+
164+
func (r *HypercoreVMReplicationResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
165+
tflog.Info(ctx, "TTRT HypercoreVMReplicationResource READ")
166+
var data HypercoreVMReplicationResourceModel
167+
// Read Terraform prior state data into the model
168+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
169+
170+
if resp.Diagnostics.HasError() {
171+
return
172+
}
173+
174+
// Boot Order read ======================================================================
175+
restClient := *r.client
176+
replicationUUID := data.Id.ValueString()
177+
vmUUID := data.VmUUID.ValueString()
178+
179+
tflog.Debug(ctx, fmt.Sprintf("TTRT HypercoreVMReplicationResource Read oldState replicationUUID=%s\n", replicationUUID))
180+
181+
pHc3Replication := utils.GetVMReplicationByUUID(restClient, replicationUUID)
182+
if pHc3Replication == nil {
183+
resp.Diagnostics.AddError("VM not found", fmt.Sprintf("VM replication not found - replicationUUID=%s", replicationUUID))
184+
return
185+
}
186+
hc3Replication := *pHc3Replication
187+
188+
tflog.Info(ctx, fmt.Sprintf("TTRT HypercoreVMReplicationResource: vm_uuid=%s, replication_uuid=%s", vmUUID, replicationUUID))
189+
190+
// save into the Terraform state.
191+
data.Id = types.StringValue(replicationUUID)
192+
data.TargetVmUUID = types.StringValue(utils.AnyToString(hc3Replication["targetDomainUUID"]))
193+
data.VmUUID = types.StringValue(utils.AnyToString(hc3Replication["sourceDomainUUID"]))
194+
data.ConnectionUUID = types.StringValue(utils.AnyToString(hc3Replication["connectionUUID"]))
195+
data.Label = types.StringValue(utils.AnyToString(hc3Replication["label"]))
196+
data.Enable = types.BoolValue(utils.AnyToBool(hc3Replication["enable"]))
197+
198+
// Save updated data into Terraform state
199+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
200+
}
201+
202+
func (r *HypercoreVMReplicationResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
203+
tflog.Info(ctx, "TTRT HypercoreVMReplicationResource UPDATE")
204+
var data_state HypercoreVMReplicationResourceModel
205+
resp.Diagnostics.Append(req.State.Get(ctx, &data_state)...)
206+
var data HypercoreVMReplicationResourceModel
207+
208+
// Read Terraform plan data into the model
209+
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
210+
211+
if resp.Diagnostics.HasError() {
212+
return
213+
}
214+
215+
restClient := *r.client
216+
replicationUUID := data.Id.ValueString()
217+
vmUUID := data.VmUUID.ValueString()
218+
connectionUUID := data.ConnectionUUID.ValueString()
219+
label := data.Label.ValueString()
220+
enable := data.Enable.ValueBool()
221+
222+
diag := utils.UpdateVMReplication(restClient, replicationUUID, vmUUID, connectionUUID, label, enable, ctx)
223+
if diag != nil {
224+
resp.Diagnostics.AddWarning(diag.Summary(), diag.Detail())
225+
}
226+
227+
// TODO: Check if HC3 matches TF
228+
// Do not trust UpdateVMReplication made what we asked for. Read new power state from HC3.
229+
pHc3Replication := utils.GetVMReplicationByUUID(restClient, replicationUUID)
230+
if pHc3Replication == nil {
231+
msg := fmt.Sprintf("VM replication not found - replicationUUID=%s.", replicationUUID)
232+
resp.Diagnostics.AddError("VM replication not found", msg)
233+
return
234+
}
235+
newHc3Replication := *pHc3Replication
236+
237+
tflog.Info(ctx, fmt.Sprintf("TTRT HypercoreVMReplicationResource: replication_uuid=%s, replication=%v", replicationUUID, newHc3Replication))
238+
239+
// Save updated data into Terraform state
240+
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
241+
}
242+
243+
func (r *HypercoreVMReplicationResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
244+
tflog.Info(ctx, "TTRT HypercoreVMReplicationResource DELETE")
245+
var data HypercoreVMReplicationResourceModel
246+
247+
// Read Terraform prior state data into the model
248+
resp.Diagnostics.Append(req.State.Get(ctx, &data)...)
249+
250+
if resp.Diagnostics.HasError() {
251+
return
252+
}
253+
254+
// Extra implementation not needed - VirDomainReplication doesn't have a DELETE endpoint
255+
256+
// If applicable, this is a great opportunity to initialize any necessary
257+
// provider client data and make a call using it.
258+
// httpResp, err := r.client.Do(httpReq)
259+
// if err != nil {
260+
// resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete example, got error: %s", err))
261+
// return
262+
// }
263+
}
264+
265+
func (r *HypercoreVMReplicationResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
266+
tflog.Info(ctx, "TTRT HypercoreVMReplicationResource IMPORT_STATE")
267+
268+
replicationUUID := req.ID
269+
tflog.Info(ctx, fmt.Sprintf("TTRT HypercoreVMReplicationResource: replicationUUID=%s", replicationUUID))
270+
271+
restClient := *r.client
272+
hc3Replication := utils.GetVMReplicationByUUID(restClient, replicationUUID)
273+
274+
if hc3Replication == nil {
275+
msg := fmt.Sprintf("VM Replication import, VM not found - 'replication_uuid'='%s'.", req.ID)
276+
resp.Diagnostics.AddError("VM Replication import error, VM not found", msg)
277+
return
278+
}
279+
280+
vmUUID := utils.AnyToString((*hc3Replication)["sourceDomainUUID"])
281+
targetVmUUID := utils.AnyToString((*hc3Replication)["targetDomainUUID"])
282+
connectionUUID := utils.AnyToString((*hc3Replication)["connectionUUID"])
283+
label := utils.AnyToString((*hc3Replication)["label"])
284+
enable := utils.AnyToBool((*hc3Replication)["enable"])
285+
286+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("id"), replicationUUID)...)
287+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("target_vm_uuid"), targetVmUUID)...)
288+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("vm_uuid"), vmUUID)...)
289+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("connection_uuid"), connectionUUID)...)
290+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("label"), label)...)
291+
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("enable"), enable)...)
292+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ func (p *HypercoreProvider) Resources(ctx context.Context) []func() resource.Res
171171
NewHypercoreISOResource,
172172
NewHypercoreVMPowerStateResource,
173173
NewHypercoreVMBootOrderResource,
174+
NewHypercoreVMReplicationResource,
174175
}
175176
}
176177

internal/utils/vm_replication.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package utils
5+
6+
import (
7+
"context"
8+
"fmt"
9+
10+
"github.com/hashicorp/terraform-plugin-framework/diag"
11+
"github.com/hashicorp/terraform-plugin-log/tflog"
12+
)
13+
14+
func GetVMReplicationByUUID(
15+
restClient RestClient,
16+
replicationUUID string,
17+
) *map[string]any {
18+
replication := restClient.GetRecord(
19+
fmt.Sprintf("/rest/v1/VirDomainReplication/%s", replicationUUID),
20+
nil,
21+
false,
22+
-1,
23+
)
24+
return replication
25+
}
26+
27+
func CreateVMReplication(
28+
restClient RestClient,
29+
sourceVmUUID string,
30+
connectionUUID string,
31+
label string,
32+
enable bool,
33+
ctx context.Context,
34+
) (string, map[string]any) {
35+
payload := map[string]any{
36+
"sourceDomainUUID": sourceVmUUID,
37+
"connectionUUID": connectionUUID,
38+
"label": label,
39+
"enable": enable,
40+
}
41+
taskTag, _, _ := restClient.CreateRecord(
42+
"/rest/v1/VirDomainReplication",
43+
payload,
44+
-1,
45+
)
46+
taskTag.WaitTask(restClient, ctx)
47+
replicationUUID := taskTag.CreatedUUID
48+
replication := GetVMReplicationByUUID(restClient, replicationUUID)
49+
return replicationUUID, *replication
50+
}
51+
52+
func UpdateVMReplication(
53+
restClient RestClient,
54+
replicationUUID string,
55+
sourceVmUUID string,
56+
connectionUUID string,
57+
label string,
58+
enable bool,
59+
ctx context.Context,
60+
) diag.Diagnostic {
61+
payload := map[string]any{
62+
"sourceDomainUUID": sourceVmUUID,
63+
"connectionUUID": connectionUUID,
64+
"label": label,
65+
"enable": enable,
66+
}
67+
68+
taskTag, err := restClient.UpdateRecord(
69+
fmt.Sprintf("/rest/v1/VirDomainReplication/%s", replicationUUID),
70+
payload,
71+
-1,
72+
ctx,
73+
)
74+
75+
if err != nil {
76+
return diag.NewWarningDiagnostic(
77+
"HC3 is receiving too many requests at the same time.",
78+
fmt.Sprintf("Please retry apply after Terraform finishes it's current operation. HC3 response message: %v", err.Error()),
79+
)
80+
}
81+
82+
taskTag.WaitTask(restClient, ctx)
83+
tflog.Debug(ctx, fmt.Sprintf("TTRT Task Tag: %v\n", taskTag))
84+
85+
return nil
86+
}

0 commit comments

Comments
 (0)