diff --git a/cloudstack/data_source_cloudstack_user_data.go b/cloudstack/data_source_cloudstack_user_data.go new file mode 100644 index 00000000..29ef5019 --- /dev/null +++ b/cloudstack/data_source_cloudstack_user_data.go @@ -0,0 +1,128 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "encoding/base64" + "fmt" + "log" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceCloudstackUserData() *schema.Resource { + return &schema.Resource{ + Read: dataSourceCloudstackUserDataRead, + Schema: map[string]*schema.Schema{ + "filter": dataSourceFiltersSchema(), + "account": { + Type: schema.TypeString, + Computed: true, + }, + "account_id": { + Type: schema.TypeString, + Computed: true, + }, + "domain": { + Type: schema.TypeString, + Computed: true, + }, + "domain_id": { + Type: schema.TypeString, + Computed: true, + }, + "project": { + Type: schema.TypeString, + Computed: true, + }, + "project_id": { + Type: schema.TypeString, + Computed: true, + }, + "userdata_id": { + Type: schema.TypeString, + Computed: true, + }, + "userdata": { + Type: schema.TypeString, + Computed: true, + }, + "params": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func dataSourceCloudstackUserDataRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + name := d.Get("name").(string) + p := cs.User.NewListUserDataParams() + p.SetName(name) + + if v, ok := d.GetOk("account"); ok { + p.SetAccount(v.(string)) + } + if v, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(v.(string)) + } + + log.Printf("[DEBUG] Listing user data with name: %s", name) + userdataList, err := cs.User.ListUserData(p) + if err != nil { + return fmt.Errorf("Error listing user data with name %s: %s", name, err) + } + + if len(userdataList.UserData) == 0 { + return fmt.Errorf("No user data found with name: %s", name) + } + if len(userdataList.UserData) > 1 { + return fmt.Errorf("Multiple user data entries found with name: %s", name) + } + + userdata := userdataList.UserData[0] + + d.SetId(userdata.Id) + d.Set("name", userdata.Name) + d.Set("account", userdata.Account) + d.Set("account_id", userdata.Accountid) + d.Set("domain", userdata.Domain) + d.Set("domain_id", userdata.Domainid) + d.Set("userdata_id", userdata.Id) + d.Set("params", userdata.Params) + + if userdata.Project != "" { + d.Set("project", userdata.Project) + d.Set("project_id", userdata.Projectid) + } + + if userdata.Userdata != "" { + decoded, err := base64.StdEncoding.DecodeString(userdata.Userdata) + if err != nil { + d.Set("userdata", userdata.Userdata) // Fallback: use raw data + } else { + d.Set("userdata", string(decoded)) + } + } + return nil +} diff --git a/cloudstack/provider.go b/cloudstack/provider.go index 63d02349..72090147 100644 --- a/cloudstack/provider.go +++ b/cloudstack/provider.go @@ -104,6 +104,7 @@ func Provider() *schema.Provider { "cloudstack_quota": dataSourceCloudStackQuota(), "cloudstack_quota_enabled": dataSourceCloudStackQuotaEnabled(), "cloudstack_quota_tariff": dataSourceCloudStackQuotaTariff(), + "cloudstack_user_data": dataSourceCloudstackUserData(), }, ResourcesMap: map[string]*schema.Resource{ @@ -164,6 +165,7 @@ func Provider() *schema.Provider { "cloudstack_limits": resourceCloudStackLimits(), "cloudstack_snapshot_policy": resourceCloudStackSnapshotPolicy(), "cloudstack_quota_tariff": resourceCloudStackQuotaTariff(), + "cloudstack_user_data": resourceCloudStackUserData(), }, ConfigureFunc: providerConfigure, diff --git a/cloudstack/resource_cloudstack_instance.go b/cloudstack/resource_cloudstack_instance.go index 4caa345d..6a38ddb4 100644 --- a/cloudstack/resource_cloudstack_instance.go +++ b/cloudstack/resource_cloudstack_instance.go @@ -212,6 +212,17 @@ func resourceCloudStackInstance() *schema.Resource { }, }, + "userdata_id": { + Type: schema.TypeString, + Optional: true, + }, + + "userdata_details": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "details": { Type: schema.TypeMap, Optional: true, @@ -446,6 +457,20 @@ func resourceCloudStackInstanceCreate(d *schema.ResourceData, meta interface{}) p.SetUserdata(ud) } + if userdataID, ok := d.GetOk("userdata_id"); ok { + p.SetUserdataid(userdataID.(string)) + } + + if userdataDetails, ok := d.GetOk("userdata_details"); ok { + udDetails := make(map[string]string) + index := 0 + for k, v := range userdataDetails.(map[string]interface{}) { + udDetails[fmt.Sprintf("userdatadetails[%d].%s", index, k)] = v.(string) + index++ + } + p.SetUserdatadetails(udDetails) + } + // Create the new instance r, err := cs.VirtualMachine.DeployVirtualMachine(p) if err != nil { @@ -560,6 +585,23 @@ func resourceCloudStackInstanceRead(d *schema.ResourceData, meta interface{}) er d.Set("boot_mode", vm.Bootmode) } + if vm.Userdataid != "" { + d.Set("userdata_id", vm.Userdataid) + } + + if vm.Userdata != "" { + decoded, err := base64.StdEncoding.DecodeString(vm.Userdata) + if err != nil { + d.Set("user_data", vm.Userdata) + } else { + d.Set("user_data", string(decoded)) + } + } + + if vm.Userdatadetails != "" { + log.Printf("[DEBUG] Instance %s has userdata details: %s", vm.Name, vm.Userdatadetails) + } + return nil } @@ -609,7 +651,8 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) // Attributes that require reboot to update if d.HasChange("name") || d.HasChange("service_offering") || d.HasChange("affinity_group_ids") || - d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("keypairs") || d.HasChange("user_data") { + d.HasChange("affinity_group_names") || d.HasChange("keypair") || d.HasChange("keypairs") || + d.HasChange("user_data") || d.HasChange("userdata_id") || d.HasChange("userdata_details") { // Before we can actually make these changes, the virtual machine must be stopped _, err := cs.VirtualMachine.StopVirtualMachine( @@ -763,6 +806,40 @@ func resourceCloudStackInstanceUpdate(d *schema.ResourceData, meta interface{}) } } + if d.HasChange("userdata_id") { + log.Printf("[DEBUG] userdata_id changed for %s, starting update", name) + + p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) + if userdataID, ok := d.GetOk("userdata_id"); ok { + p.SetUserdataid(userdataID.(string)) + } + _, err := cs.VirtualMachine.UpdateVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error updating userdata_id for instance %s: %s", name, err) + } + } + + if d.HasChange("userdata_details") { + log.Printf("[DEBUG] userdata_details changed for %s, starting update", name) + + p := cs.VirtualMachine.NewUpdateVirtualMachineParams(d.Id()) + if userdataDetails, ok := d.GetOk("userdata_details"); ok { + udDetails := make(map[string]string) + index := 0 + for k, v := range userdataDetails.(map[string]interface{}) { + udDetails[fmt.Sprintf("userdatadetails[%d].%s", index, k)] = v.(string) + index++ + } + p.SetUserdatadetails(udDetails) + } + _, err := cs.VirtualMachine.UpdateVirtualMachine(p) + if err != nil { + return fmt.Errorf( + "Error updating userdata_details for instance %s: %s", name, err) + } + } + // Start the virtual machine again _, err = cs.VirtualMachine.StartVirtualMachine( cs.VirtualMachine.NewStartVirtualMachineParams(d.Id())) diff --git a/cloudstack/resource_cloudstack_template.go b/cloudstack/resource_cloudstack_template.go index 4316c7eb..ced400a5 100644 --- a/cloudstack/resource_cloudstack_template.go +++ b/cloudstack/resource_cloudstack_template.go @@ -133,6 +133,37 @@ func resourceCloudStackTemplate() *schema.Resource { ForceNew: true, }, + "userdata_link": { + Type: schema.TypeList, + Optional: true, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "userdata_id": { + Type: schema.TypeString, + Required: true, + Description: "The ID of the user data to link to the template.", + }, + "userdata_policy": { + Type: schema.TypeString, + Optional: true, + Default: "ALLOWOVERRIDE", + Description: "Override policy of the userdata. Possible values: ALLOWOVERRIDE, APPEND, DENYOVERRIDE. Default: ALLOWOVERRIDE", + }, + "userdata_name": { + Type: schema.TypeString, + Computed: true, + Description: "The name of the linked user data.", + }, + "userdata_params": { + Type: schema.TypeString, + Computed: true, + Description: "The parameters of the linked user data.", + }, + }, + }, + }, + "tags": tagsSchema(), }, } @@ -224,6 +255,11 @@ func resourceCloudStackTemplateCreate(d *schema.ResourceData, meta interface{}) return fmt.Errorf("Error setting tags on the template %s: %s", name, err) } + // Link userdata if specified + if err = linkUserdataToTemplate(cs, d, r.RegisterTemplate[0].Id); err != nil { + return fmt.Errorf("Error linking userdata to template %s: %s", name, err) + } + // Wait until the template is ready to use, or timeout with an error... currentTime := time.Now().Unix() timeout := int64(d.Get("is_ready_timeout").(int)) @@ -300,6 +336,11 @@ func resourceCloudStackTemplateRead(d *schema.ResourceData, meta interface{}) er setValueOrID(d, "project", t.Project, t.Projectid) setValueOrID(d, "zone", t.Zonename, t.Zoneid) + // Read userdata link information + if err := readUserdataFromTemplate(d, t); err != nil { + return fmt.Errorf("Error reading userdata link from template: %s", err) + } + return nil } @@ -349,6 +390,12 @@ func resourceCloudStackTemplateUpdate(d *schema.ResourceData, meta interface{}) } } + if d.HasChange("userdata_link") { + if err := updateUserdataLink(cs, d); err != nil { + return fmt.Errorf("Error updating userdata link for template %s: %s", name, err) + } + } + return resourceCloudStackTemplateRead(d, meta) } @@ -383,3 +430,105 @@ func verifyTemplateParams(d *schema.ResourceData) error { return nil } + +func linkUserdataToTemplate(cs *cloudstack.CloudStackClient, d *schema.ResourceData, templateID string) error { + userdataLinks := d.Get("userdata_link").([]interface{}) + if len(userdataLinks) == 0 { + return nil + } + + userdataLink := userdataLinks[0].(map[string]interface{}) + + p := cs.Template.NewLinkUserDataToTemplateParams() + p.SetTemplateid(templateID) + p.SetUserdataid(userdataLink["userdata_id"].(string)) + + if policy, ok := userdataLink["userdata_policy"].(string); ok && policy != "" { + p.SetUserdatapolicy(policy) + } + + _, err := cs.Template.LinkUserDataToTemplate(p) + return err +} + +func readUserdataFromTemplate(d *schema.ResourceData, template *cloudstack.Template) error { + if template.Userdataid == "" { + d.Set("userdata_link", []interface{}{}) + return nil + } + + userdataLink := map[string]interface{}{ + "userdata_id": template.Userdataid, + "userdata_name": template.Userdataname, + "userdata_params": template.Userdataparams, + } + + if existingLinks := d.Get("userdata_link").([]interface{}); len(existingLinks) > 0 { + if existingLink, ok := existingLinks[0].(map[string]interface{}); ok { + if policy, exists := existingLink["userdata_policy"]; exists { + userdataLink["userdata_policy"] = policy + } + } + } + + d.Set("userdata_link", []interface{}{userdataLink}) + return nil +} + +func updateUserdataLink(cs *cloudstack.CloudStackClient, d *schema.ResourceData) error { + templateID := d.Id() + + oldLinks, newLinks := d.GetChange("userdata_link") + oldLinksSlice := oldLinks.([]interface{}) + newLinksSlice := newLinks.([]interface{}) + + // Check if we're removing userdata link (had one before, now empty) + if len(oldLinksSlice) > 0 && len(newLinksSlice) == 0 { + unlinkP := cs.Template.NewLinkUserDataToTemplateParams() + unlinkP.SetTemplateid(templateID) + + _, err := cs.Template.LinkUserDataToTemplate(unlinkP) + if err != nil { + return fmt.Errorf("Error unlinking userdata from template: %s", err) + } + log.Printf("[DEBUG] Unlinked userdata from template: %s", templateID) + return nil + } + + if len(newLinksSlice) > 0 { + newLink := newLinksSlice[0].(map[string]interface{}) + + if len(oldLinksSlice) > 0 { + oldLink := oldLinksSlice[0].(map[string]interface{}) + + if oldLink["userdata_id"].(string) == newLink["userdata_id"].(string) { + oldPolicy := "" + newPolicy := "" + + if p, ok := oldLink["userdata_policy"].(string); ok { + oldPolicy = p + } + if p, ok := newLink["userdata_policy"].(string); ok { + newPolicy = p + } + + if oldPolicy == newPolicy { + log.Printf("[DEBUG] Userdata link unchanged, skipping API call") + return nil + } + } + + unlinkP := cs.Template.NewLinkUserDataToTemplateParams() + unlinkP.SetTemplateid(templateID) + + _, err := cs.Template.LinkUserDataToTemplate(unlinkP) + if err != nil { + log.Printf("[DEBUG] Error unlinking existing userdata (this may be normal): %s", err) + } + } + + return linkUserdataToTemplate(cs, d, templateID) + } + + return nil +} diff --git a/cloudstack/resource_cloudstack_user_data.go b/cloudstack/resource_cloudstack_user_data.go new file mode 100644 index 00000000..65a3d2ca --- /dev/null +++ b/cloudstack/resource_cloudstack_user_data.go @@ -0,0 +1,166 @@ +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. +// + +package cloudstack + +import ( + "fmt" + "strings" + + "github.com/apache/cloudstack-go/v2/cloudstack" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceCloudStackUserData() *schema.Resource { + return &schema.Resource{ + Create: resourceCloudStackUserDataCreate, + Read: resourceCloudStackUserDataRead, + Delete: resourceCloudStackUserDataDelete, + Importer: &schema.ResourceImporter{ + State: importStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Name of the user data", + }, + + "userdata": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The user data content to be registered", + }, + + "account": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional account for the user data. Must be used with domain_id.", + }, + + "domain_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional domain ID for the user data. If the account parameter is used, domain_id must also be used.", + }, + + "params": { + Type: schema.TypeSet, + Optional: true, + ForceNew: true, + Description: "Optional comma separated list of variables declared in user data content.", + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + + "project_id": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "An optional project for the user data.", + }, + }, + } +} + +func resourceCloudStackUserDataCreate(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.User.NewRegisterUserDataParams(d.Get("name").(string), d.Get("userdata").(string)) + if v, ok := d.GetOk("account"); ok { + p.SetAccount(v.(string)) + } + if v, ok := d.GetOk("domain_id"); ok { + p.SetDomainid(v.(string)) + } + if v, ok := d.GetOk("project_id"); ok { + p.SetProjectid(v.(string)) + } + if v, ok := d.GetOk("params"); ok { + paramsList := v.(*schema.Set).List() + var params []string + for _, param := range paramsList { + params = append(params, param.(string)) + } + p.SetParams(strings.Join(params, ",")) + } + + userdata, err := cs.User.RegisterUserData(p) + if err != nil { + return fmt.Errorf("Error registering user data: %s", err) + } + + d.SetId(userdata.Id) + + return resourceCloudStackUserDataRead(d, meta) +} + +func resourceCloudStackUserDataRead(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + id := d.Id() + + p := cs.User.NewListUserDataParams() + p.SetId(id) + + userdata, err := cs.User.ListUserData(p) + if err != nil { + return fmt.Errorf("Error retrieving user data with ID %s: %s", id, err) + } + + d.Set("name", userdata.UserData[0].Name) + d.Set("userdata", userdata.UserData[0].Userdata) + if d.Get("account").(string) != "" { + d.Set("account", userdata.UserData[0].Account) + } + if d.Get("domain_id").(string) != "" { + d.Set("domain_id", userdata.UserData[0].Domainid) + } + if userdata.UserData[0].Params != "" { + paramsList := strings.Split(userdata.UserData[0].Params, ",") + var paramsSet []interface{} + for _, param := range paramsList { + paramsSet = append(paramsSet, param) + } + d.Set("params", schema.NewSet(schema.HashString, paramsSet)) + } + if userdata.UserData[0].Projectid != "" { + d.Set("project_id", userdata.UserData[0].Projectid) + } + + return nil +} + +func resourceCloudStackUserDataDelete(d *schema.ResourceData, meta interface{}) error { + cs := meta.(*cloudstack.CloudStackClient) + + p := cs.User.NewDeleteUserDataParams(d.Id()) + _, err := cs.User.DeleteUserData(p) + if err != nil { + return fmt.Errorf("Error deleting user data with ID %s: %s", d.Id(), err) + } + + return nil +} diff --git a/website/docs/README.md b/website/docs/README.md index 0c09b532..2fe8dd3f 100644 --- a/website/docs/README.md +++ b/website/docs/README.md @@ -66,6 +66,7 @@ The following arguments are supported: - [ssh_keypair](./d/ssh_keypair.html.markdown) - [template](./d/template.html.markdown) - [user](./d/user.html.markdown) +- [user_data](./d/user_data.html.markdown) - [volume](./d/volume.html.markdown) - [vpc](./d/vpc.html.markdown) - [vpn_connection](./d/vpn_connection.html.markdown) @@ -101,6 +102,7 @@ The following arguments are supported: - [static_route](./r/static_route.html.markdown) - [template](./r/template.html.markdown) - [user](./r/user.html.markdown) +- [userdata](./r/userdata.html.markdown) - [volume](./r/volume.html.markdown) - [vpc](./r/vpc.html.markdown) - [vpn_connection](./r/vpn_connection.html.markdown) diff --git a/website/docs/d/user_data.html.markdown b/website/docs/d/user_data.html.markdown new file mode 100644 index 00000000..1e431324 --- /dev/null +++ b/website/docs/d/user_data.html.markdown @@ -0,0 +1,126 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_user_data" +sidebar_current: "docs-cloudstack-datasource-user-data" +description: |- + Get information about a CloudStack user data. +--- + +# cloudstack_user_data + +Use this data source to retrieve information about a CloudStack user data by either its name or ID. + +## Example Usage + +### Find User Data by Name + +```hcl +data "cloudstack_user_data" "web_init" { + filter { + name = "name" + value = "web-server-init" + } +} + +# Use the user data in an instance +resource "cloudstack_instance" "web" { + name = "web-server" + userdata_id = data.cloudstack_user_data.web_init.id + # ... other arguments ... +} +``` + +### Find User Data by ID + +```hcl +data "cloudstack_user_data" "app_init" { + filter { + name = "id" + value = "12345678-1234-1234-1234-123456789012" + } +} +``` + +### Find Project-Scoped User Data + +```hcl +data "cloudstack_user_data" "project_init" { + project = "my-project" + + filter { + name = "name" + value = "project-specific-init" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `filter` - (Required) One or more name/value pairs to filter off of. You can apply multiple filters to narrow down the results. See [Filters](#filters) below for more details. + +* `project` - (Optional) The name or ID of the project to search in. + +### Filters + +The `filter` block supports the following arguments: + +* `name` - (Required) The name of the filter. Valid filter names are: + * `id` - Filter by user data ID + * `name` - Filter by user data name + * `account` - Filter by account name + * `domainid` - Filter by domain ID + +* `value` - (Required) The value to filter by. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The user data ID. +* `name` - The name of the user data. +* `userdata` - The user data content. +* `account` - The account name owning the user data. +* `domain_id` - The domain ID where the user data belongs. +* `project_id` - The project ID if the user data is project-scoped. +* `params` - The list of parameter names defined in the user data (comma-separated string). + +## Example with Template Integration + +```hcl +# Find existing user data +data "cloudstack_user_data" "app_bootstrap" { + filter { + name = "name" + value = "application-bootstrap" + } +} + +# Use with template +resource "cloudstack_template" "app_template" { + name = "application-template" + display_text = "Application Template with Bootstrap" + # ... other template arguments ... + + userdata_link { + userdata_id = data.cloudstack_user_data.app_bootstrap.id + userdata_policy = "ALLOWOVERRIDE" + } +} + +# Deploy instance with parameterized user data +resource "cloudstack_instance" "app_server" { + name = "app-server-01" + template = cloudstack_template.app_template.id + # ... other instance arguments ... + + userdata_id = data.cloudstack_user_data.app_bootstrap.id + + userdata_details = { + "environment" = "production" + "app_version" = "v2.1.0" + "debug_enabled" = "false" + } +} +``` diff --git a/website/docs/r/instance.html.markdown b/website/docs/r/instance.html.markdown index 7b0b1bdd..ebf3f20f 100644 --- a/website/docs/r/instance.html.markdown +++ b/website/docs/r/instance.html.markdown @@ -13,6 +13,8 @@ disk offering, and template. ## Example Usage +### Basic Instance + ```hcl resource "cloudstack_instance" "web" { name = "server-1" @@ -23,6 +25,86 @@ resource "cloudstack_instance" "web" { } ``` +### Instance with Inline User Data + +```hcl +resource "cloudstack_instance" "web_with_userdata" { + name = "web-server" + service_offering = "small" + network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" + template = "Ubuntu 20.04" + zone = "zone-1" + + user_data = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + systemctl enable nginx + systemctl start nginx + EOF + ) +} +``` + +### Instance with Registered User Data + +```hcl +# First, create registered user data +resource "cloudstack_userdata" "web_init" { + name = "web-server-init" + + userdata = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + + # Use parameters + echo "

Welcome to $${app_name}!

" > /var/www/html/index.html + echo "

Environment: $${environment}

" >> /var/www/html/index.html + + systemctl enable nginx + systemctl start nginx + EOF + ) + + params = ["app_name", "environment"] +} + +# Deploy instance with parameterized user data +resource "cloudstack_instance" "app_server" { + name = "app-server-01" + service_offering = "medium" + network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" + template = "Ubuntu 20.04" + zone = "zone-1" + + userdata_id = cloudstack_userdata.web_init.id + + userdata_details = { + "app_name" = "My Application" + "environment" = "production" + } +} +``` + +### Instance with Template-Linked User Data + +```hcl +# Use a template that has user data pre-linked +resource "cloudstack_instance" "from_template" { + name = "template-instance" + service_offering = "small" + network_id = "6eb22f91-7454-4107-89f4-36afcdf33021" + template = cloudstack_template.web_template.id # Template with userdata_link + zone = "zone-1" + + # Override parameters for the template's linked user data + userdata_details = { + "app_name" = "Template-Based App" + } +} +``` + ## Argument Reference The following arguments are supported: @@ -93,6 +175,10 @@ The following arguments are supported: * `user_data` - (Optional) The user data to provide when launching the instance. This can be either plain text or base64 encoded text. +* `userdata_id` - (Optional) The ID of a registered CloudStack user data to use for this instance. Cannot be used together with `user_data`. + +* `userdata_details` - (Optional) A map of key-value pairs to pass as parameters to the user data script. Only valid when `userdata_id` is specified. Keys must match the parameter names defined in the user data. + * `keypair` - (Optional) The name of the SSH key pair that will be used to access this instance. (Mutual exclusive with keypairs) diff --git a/website/docs/r/template.html.markdown b/website/docs/r/template.html.markdown index 52eb7fa3..1f7a7d18 100644 --- a/website/docs/r/template.html.markdown +++ b/website/docs/r/template.html.markdown @@ -82,6 +82,10 @@ The following arguments are supported: * `for_cks` - (Optional) Set to `true` to indicate this template is for CloudStack Kubernetes Service (CKS). CKS templates have special requirements and capabilities. Defaults to `false`. +### User Data Integration + +* `userdata_link` - (Optional) Link user data to this template. When specified, instances deployed from this template will inherit the linked user data. See [User Data Link](#user-data-link) below for more details. + ### Template Properties * `is_dynamically_scalable` - (Optional) Set to indicate if the template contains tools to support dynamic scaling of VM cpu/memory. Defaults to `false`. @@ -136,6 +140,70 @@ The following attributes are exported: * `domain` - The domain name where the template belongs. * `project` - The project name if the template is assigned to a project. +## User Data Link + +The `userdata_link` block supports the following arguments: + +* `userdata_id` - (Required) The ID of the user data to link to this template. +* `userdata_policy` - (Required) The user data policy for instances deployed from this template. Valid values: + * `ALLOWOVERRIDE` - Allow instances to override the linked user data with their own + * `APPEND` - Append instance-specific user data to the template's linked user data + * `DENYOVERRIDE` - Prevent instances from overriding the linked user data + +When a `userdata_link` is configured, the following additional attributes are exported: + +* `userdata_name` - The name of the linked user data +* `userdata_params` - The parameters defined in the linked user data + +### Example Template with User Data + +```hcl +# Create user data +resource "cloudstack_userdata" "web_init" { + name = "web-server-initialization" + + userdata = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + echo "

Welcome to $${app_name}!

" > /var/www/html/index.html + systemctl enable nginx + systemctl start nginx + EOF + ) + + params = ["app_name"] +} + +# Create template with linked user data +resource "cloudstack_template" "web_template" { + name = "web-server-template" + display_text = "Web Server Template with Auto-Setup" + format = "QCOW2" + hypervisor = "KVM" + os_type = "Ubuntu 20.04" + url = "http://example.com/ubuntu-20.04.qcow2" + zone = "zone1" + + userdata_link { + userdata_id = cloudstack_userdata.web_init.id + userdata_policy = "ALLOWOVERRIDE" + } +} + +# Deploy instance using template with user data +resource "cloudstack_instance" "web_server" { + name = "web-01" + template = cloudstack_template.web_template.id + # ... other arguments ... + + # Pass parameters to the linked user data + userdata_details = { + "app_name" = "Production Web App" + } +} +``` + ### Example CKS Template Usage ```hcl diff --git a/website/docs/r/userdata.html.markdown b/website/docs/r/userdata.html.markdown new file mode 100644 index 00000000..58c65fbb --- /dev/null +++ b/website/docs/r/userdata.html.markdown @@ -0,0 +1,150 @@ +--- +layout: "cloudstack" +page_title: "CloudStack: cloudstack_user_data" +sidebar_current: "docs-cloudstack-resource-user-data" +description: |- + Registers and manages user data in CloudStack for VM initialization. +--- + +# cloudstack_user_data + +Registers user data in CloudStack that can be used to initialize virtual machines during deployment. User data typically contains scripts, configuration files, or other initialization data that should be executed when a VM starts. + +## Example Usage + +### Basic User Data + +```hcl +resource "cloudstack_user_data" "web_init" { + name = "web-server-init" + + userdata = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + systemctl enable nginx + systemctl start nginx + EOF + ) +} +``` + +### Parameterized User Data + +```hcl +resource "cloudstack_user_data" "app_init" { + name = "app-server-init" + + userdata = base64encode(<<-EOF + #!/bin/bash + apt-get update + apt-get install -y nginx + + # Use parameters passed from instance deployment + echo "

Welcome to $${app_name}!

" > /var/www/html/index.html + echo "

Environment: $${environment}

" >> /var/www/html/index.html + echo "

Debug Mode: $${debug_mode}

" >> /var/www/html/index.html + + systemctl enable nginx + systemctl start nginx + EOF + ) + + # Define parameters that can be passed during instance deployment + params = ["app_name", "environment", "debug_mode"] +} +``` + +### Project-Scoped User Data + +```hcl +resource "cloudstack_user_data" "project_init" { + name = "project-specific-init" + project_id = "12345678-1234-1234-1234-123456789012" + + userdata = base64encode(<<-EOF + #!/bin/bash + # Project-specific initialization + echo "Initializing project environment..." + EOF + ) +} +``` + +## Argument Reference + +The following arguments are supported: + +### Required Arguments + +* `name` - (Required) The name of the user data. Must be unique within the account/project scope. +* `userdata` - (Required) The user data content to be registered. Should be base64 encoded. This is typically a cloud-init script or other initialization data. + +### Optional Arguments + +* `account` - (Optional) The account name for the user data. Must be used together with `domain_id`. If not specified, uses the current account. +* `domain_id` - (Optional) The domain ID for the user data. Required when `account` is specified. +* `project_id` - (Optional) The project ID to create this user data for. Cannot be used together with `account`/`domain_id`. +* `params` - (Optional) A list of parameter names that are declared in the user data content. These parameters can be passed values during instance deployment using `userdata_details`. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The user data ID. +* `name` - The name of the user data. +* `userdata` - The registered user data content. +* `account` - The account name owning the user data. +* `domain_id` - The domain ID where the user data belongs. +* `project_id` - The project ID if the user data is project-scoped. +* `params` - The list of parameter names defined in the user data. + +## Usage with Templates and Instances + +User data can be used in multiple ways: + +### 1. Linked to Templates + +```hcl +resource "cloudstack_template" "web_template" { + name = "web-server-template" + # ... other template arguments ... + + userdata_link { + userdata_id = cloudstack_user_data.app_init.id + userdata_policy = "ALLOWOVERRIDE" # Allow instance to override + } +} +``` + +### 2. Direct Instance Usage + +```hcl +resource "cloudstack_instance" "web_server" { + name = "web-server-01" + # ... other instance arguments ... + + userdata_id = cloudstack_user_data.app_init.id # Pass parameter values to the userdata script + userdata_details = { + "app_name" = "My Web Application" + "environment" = "production" + "debug_mode" = "false" + } +} +``` + +## Import + +User data can be imported using the user data ID: + +``` +terraform import cloudstack_user_data.example 12345678-1234-1234-1234-123456789012 +``` + +## Notes + +* User data content should be base64 encoded before registration +* Parameter substitution in user data uses the format `${parameter_name}` +* Parameters must be declared in the `params` list to be usable +* User data is immutable after creation - changes require resource recreation +* Maximum user data size depends on CloudStack configuration (typically 32KB)