Skip to content

Commit 052a0e6

Browse files
committed
Add support for userdata and linking to templates
1 parent 7267192 commit 052a0e6

File tree

7 files changed

+671
-1
lines changed

7 files changed

+671
-1
lines changed

cloudstack/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func Provider() *schema.Provider {
104104
"cloudstack_quota": dataSourceCloudStackQuota(),
105105
"cloudstack_quota_enabled": dataSourceCloudStackQuotaEnabled(),
106106
"cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(),
107+
"cloudstack_user_data": dataSourceCloudstackUserData(),
107108
},
108109

109110
ResourcesMap: map[string]*schema.Resource{
@@ -164,6 +165,7 @@ func Provider() *schema.Provider {
164165
"cloudstack_limits": resourceCloudStackLimits(),
165166
"cloudstack_snapshot_policy": resourceCloudStackSnapshotPolicy(),
166167
"cloudstack_quota_tariff": resourceCloudStackQuotaTariff(),
168+
"cloudstack_userdata": resourceCloudStackUserData(),
167169
},
168170

169171
ConfigureFunc: providerConfigure,

cloudstack/resource_cloudstack_instance.go

Lines changed: 88 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,17 @@ func resourceCloudStackInstance() *schema.Resource {
212212
},
213213
},
214214

215+
"userdata_id": {
216+
Type: schema.TypeString,
217+
Optional: true,
218+
},
219+
220+
"userdata_details": {
221+
Type: schema.TypeMap,
222+
Optional: true,
223+
Elem: &schema.Schema{Type: schema.TypeString},
224+
},
225+
215226
"details": {
216227
Type: schema.TypeMap,
217228
Optional: true,
@@ -446,6 +457,20 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{})
446457
p.SetUserdata(ud)
447458
}
448459

460+
if userdataID, ok := d.GetOk("userdata_id"); ok {
461+
p.SetUserdataid(userdataID.(string))
462+
}
463+
464+
if userdataDetails, ok := d.GetOk("userdata_details"); ok {
465+
udDetails := make(map[string]string)
466+
index := 0
467+
for k, v := range userdataDetails.(map[string]interface{}) {
468+
udDetails[fmt.Sprintf("userdatadetails[%d].%s", index, k)] = v.(string)
469+
index++
470+
}
471+
p.SetUserdatadetails(udDetails)
472+
}
473+
449474
// Create the new instance
450475
r, err := cs.VirtualMachine.DeployVirtualMachine(p)
451476
if err != nil {
@@ -560,6 +585,31 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er
560585
d.Set("boot_mode", vm.Bootmode)
561586
}
562587

588+
// Set userdata information if available
589+
if vm.Userdataid != "" {
590+
d.Set("userdata_id", vm.Userdataid)
591+
}
592+
593+
// Handle userdata with graceful Base64 decoding
594+
if vm.Userdata != "" {
595+
decoded, err := base64.StdEncoding.DecodeString(vm.Userdata)
596+
if err != nil {
597+
// If decoding fails, store the raw data
598+
d.Set("user_data", vm.Userdata)
599+
} else {
600+
// Successfully decoded, store as string
601+
d.Set("user_data", string(decoded))
602+
}
603+
}
604+
605+
// Set userdata_details if available (CloudStack might return them in a specific format)
606+
if vm.Userdatadetails != "" {
607+
// Note: CloudStack typically returns userdata details as a JSON string or key-value pairs
608+
// We may need to parse this based on the actual CloudStack response format
609+
// For now, we'll leave this as a placeholder since the exact format needs to be verified
610+
log.Printf("[DEBUG] Instance %s has userdata details: %s", vm.Name, vm.Userdatadetails)
611+
}
612+
563613
return nil
564614
}
565615

@@ -609,7 +659,8 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{})
609659

610660
// Attributes that require reboot to update
611661
if d.HasChange("name") || d.HasChange("service_offering") || d.HasChange("affinity_group_ids") ||
612-
d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("keypairs") || d.HasChange("user_data") {
662+
d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("keypairs") ||
663+
d.HasChange("user_data") || d.HasChange("userdata_id") || d.HasChange("userdata_details") {
613664

614665
// Before we can actually make these changes, the virtual machine must be stopped
615666
_, err := cs.VirtualMachine.StopVirtualMachine(
@@ -763,6 +814,42 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{})
763814
}
764815
}
765816

817+
// Check if the userdata_id has changed and if so, update it
818+
if d.HasChange("userdata_id") {
819+
log.Printf("[DEBUG] userdata_id changed for %s, starting update", name)
820+
821+
p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id())
822+
if userdataID, ok := d.GetOk("userdata_id"); ok {
823+
p.SetUserdataid(userdataID.(string))
824+
}
825+
_, err := cs.VirtualMachine.UpdateVirtualMachine(p)
826+
if err != nil {
827+
return fmt.Errorf(
828+
"Error updating userdata_id for instance %s: %s", name, err)
829+
}
830+
}
831+
832+
// Check if the userdata_details has changed and if so, update it
833+
if d.HasChange("userdata_details") {
834+
log.Printf("[DEBUG] userdata_details changed for %s, starting update", name)
835+
836+
p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id())
837+
if userdataDetails, ok := d.GetOk("userdata_details"); ok {
838+
udDetails := make(map[string]string)
839+
index := 0
840+
for k, v := range userdataDetails.(map[string]interface{}) {
841+
udDetails[fmt.Sprintf("userdatadetails[%d].%s", index, k)] = v.(string)
842+
index++
843+
}
844+
p.SetUserdatadetails(udDetails)
845+
}
846+
_, err := cs.VirtualMachine.UpdateVirtualMachine(p)
847+
if err != nil {
848+
return fmt.Errorf(
849+
"Error updating userdata_details for instance %s: %s", name, err)
850+
}
851+
}
852+
766853
// Start the virtual machine again
767854
_, err = cs.VirtualMachine.StartVirtualMachine(
768855
cs.VirtualMachine.NewStartVirtualMachineParams(d.Id()))

cloudstack/resource_cloudstack_template.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,37 @@ func resourceCloudStackTemplate() *schema.Resource {
133133
ForceNew: true,
134134
},
135135

136+
"userdata_link": {
137+
Type: schema.TypeList,
138+
Optional: true,
139+
MaxItems: 1,
140+
Elem: &schema.Resource{
141+
Schema: map[string]*schema.Schema{
142+
"userdata_id": {
143+
Type: schema.TypeString,
144+
Required: true,
145+
Description: "The ID of the user data to link to the template.",
146+
},
147+
"userdata_policy": {
148+
Type: schema.TypeString,
149+
Optional: true,
150+
Default: "ALLOWOVERRIDE",
151+
Description: "Override policy of the userdata. Possible values: ALLOWOVERRIDE, APPEND, DENYOVERRIDE. Default: ALLOWOVERRIDE",
152+
},
153+
"userdata_name": {
154+
Type: schema.TypeString,
155+
Computed: true,
156+
Description: "The name of the linked user data.",
157+
},
158+
"userdata_params": {
159+
Type: schema.TypeString,
160+
Computed: true,
161+
Description: "The parameters of the linked user data.",
162+
},
163+
},
164+
},
165+
},
166+
136167
"tags": tagsSchema(),
137168
},
138169
}
@@ -224,6 +255,11 @@ func resourceCloudStackTemplateCreate(d *schema.ResourceData, meta interface{})
224255
return fmt.Errorf("Error setting tags on the template %s: %s", name, err)
225256
}
226257

258+
// Link userdata if specified
259+
if err = linkUserdataToTemplate(cs, d, r.RegisterTemplate[0].Id); err != nil {
260+
return fmt.Errorf("Error linking userdata to template %s: %s", name, err)
261+
}
262+
227263
// Wait until the template is ready to use, or timeout with an error...
228264
currentTime := time.Now().Unix()
229265
timeout := int64(d.Get("is_ready_timeout").(int))
@@ -300,6 +336,11 @@ func resourceCloudStackTemplateRead(d *schema.ResourceData, meta interface{}) er
300336
setValueOrID(d, "project", t.Project, t.Projectid)
301337
setValueOrID(d, "zone", t.Zonename, t.Zoneid)
302338

339+
// Read userdata link information
340+
if err := readUserdataFromTemplate(d, t); err != nil {
341+
return fmt.Errorf("Error reading userdata link from template: %s", err)
342+
}
343+
303344
return nil
304345
}
305346

@@ -349,6 +390,12 @@ func resourceCloudStackTemplateUpdate(d *schema.ResourceData, meta interface{})
349390
}
350391
}
351392

393+
if d.HasChange("userdata_link") {
394+
if err := updateUserdataLink(cs, d); err != nil {
395+
return fmt.Errorf("Error updating userdata link for template %s: %s", name, err)
396+
}
397+
}
398+
352399
return resourceCloudStackTemplateRead(d, meta)
353400
}
354401

@@ -383,3 +430,105 @@ func verifyTemplateParams(d *schema.ResourceData) error {
383430

384431
return nil
385432
}
433+
434+
func linkUserdataToTemplate(cs *cloudstack.CloudStackClient, d *schema.ResourceData, templateID string) error {
435+
userdataLinks := d.Get("userdata_link").([]interface{})
436+
if len(userdataLinks) == 0 {
437+
return nil
438+
}
439+
440+
userdataLink := userdataLinks[0].(map[string]interface{})
441+
442+
p := cs.Template.NewLinkUserDataToTemplateParams()
443+
p.SetTemplateid(templateID)
444+
p.SetUserdataid(userdataLink["userdata_id"].(string))
445+
446+
if policy, ok := userdataLink["userdata_policy"].(string); ok && policy != "" {
447+
p.SetUserdatapolicy(policy)
448+
}
449+
450+
_, err := cs.Template.LinkUserDataToTemplate(p)
451+
return err
452+
}
453+
454+
func readUserdataFromTemplate(d *schema.ResourceData, template *cloudstack.Template) error {
455+
if template.Userdataid == "" {
456+
d.Set("userdata_link", []interface{}{})
457+
return nil
458+
}
459+
460+
userdataLink := map[string]interface{}{
461+
"userdata_id": template.Userdataid,
462+
"userdata_name": template.Userdataname,
463+
"userdata_params": template.Userdataparams,
464+
}
465+
466+
if existingLinks := d.Get("userdata_link").([]interface{}); len(existingLinks) > 0 {
467+
if existingLink, ok := existingLinks[0].(map[string]interface{}); ok {
468+
if policy, exists := existingLink["userdata_policy"]; exists {
469+
userdataLink["userdata_policy"] = policy
470+
}
471+
}
472+
}
473+
474+
d.Set("userdata_link", []interface{}{userdataLink})
475+
return nil
476+
}
477+
478+
func updateUserdataLink(cs *cloudstack.CloudStackClient, d *schema.ResourceData) error {
479+
templateID := d.Id()
480+
481+
oldLinks, newLinks := d.GetChange("userdata_link")
482+
oldLinksSlice := oldLinks.([]interface{})
483+
newLinksSlice := newLinks.([]interface{})
484+
485+
// Check if we're removing userdata link (had one before, now empty)
486+
if len(oldLinksSlice) > 0 && len(newLinksSlice) == 0 {
487+
unlinkP := cs.Template.NewLinkUserDataToTemplateParams()
488+
unlinkP.SetTemplateid(templateID)
489+
490+
_, err := cs.Template.LinkUserDataToTemplate(unlinkP)
491+
if err != nil {
492+
return fmt.Errorf("Error unlinking userdata from template: %s", err)
493+
}
494+
log.Printf("[DEBUG] Unlinked userdata from template: %s", templateID)
495+
return nil
496+
}
497+
498+
if len(newLinksSlice) > 0 {
499+
newLink := newLinksSlice[0].(map[string]interface{})
500+
501+
if len(oldLinksSlice) > 0 {
502+
oldLink := oldLinksSlice[0].(map[string]interface{})
503+
504+
if oldLink["userdata_id"].(string) == newLink["userdata_id"].(string) {
505+
oldPolicy := ""
506+
newPolicy := ""
507+
508+
if p, ok := oldLink["userdata_policy"].(string); ok {
509+
oldPolicy = p
510+
}
511+
if p, ok := newLink["userdata_policy"].(string); ok {
512+
newPolicy = p
513+
}
514+
515+
if oldPolicy == newPolicy {
516+
log.Printf("[DEBUG] Userdata link unchanged, skipping API call")
517+
return nil
518+
}
519+
}
520+
521+
unlinkP := cs.Template.NewLinkUserDataToTemplateParams()
522+
unlinkP.SetTemplateid(templateID)
523+
524+
_, err := cs.Template.LinkUserDataToTemplate(unlinkP)
525+
if err != nil {
526+
log.Printf("[DEBUG] Error unlinking existing userdata (this may be normal): %s", err)
527+
}
528+
}
529+
530+
return linkUserdataToTemplate(cs, d, templateID)
531+
}
532+
533+
return nil
534+
}

0 commit comments

Comments
 (0)