diff --git a/CHANGELOG.md b/CHANGELOG.md index 513d59e..5e097fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ +## 24.2.0 +NEW FEATURES: +* resource/connector_aws: support `instance_metadata` block. + ## 24.1.0 -NEW ENHANCEMENTS: +ENHANCEMENTS: * resource/cvo_gcp: fix typo on vpc3_firewall_rule_tag_name. * add logging to API calls. diff --git a/cloudmanager/client.go b/cloudmanager/client.go index dc724a7..3f2ff93 100644 --- a/cloudmanager/client.go +++ b/cloudmanager/client.go @@ -71,7 +71,7 @@ type Client struct { } // CallAWSInstanceCreate can be used to make a request to create AWS Instance -func (c *Client) CallAWSInstanceCreate(occmDetails createOCCMDetails) (string, error) { +func (c *Client) CallAWSInstanceCreate(occmDetails createAWSOCCMDetails) (string, error) { var sess *session.Session if c.AWSProfile != "" { @@ -160,6 +160,16 @@ func (c *Client) CallAWSInstanceCreate(occmDetails createOCCMDetails) (string, e // Network interfaces and an instance-level security groups may not be specified on the same request runInstancesInput.SecurityGroupIds = securityGroupIds } + runInstancesInput.MetadataOptions = &ec2.InstanceMetadataOptionsRequest{} + if occmDetails.InstanceMetadata.HTTPEndpoint != nil { + runInstancesInput.MetadataOptions.HttpEndpoint = aws.String(*occmDetails.InstanceMetadata.HTTPEndpoint) + } + if occmDetails.InstanceMetadata.HTTPPutResponseHopLimit != nil { + runInstancesInput.MetadataOptions.HttpPutResponseHopLimit = aws.Int64(*occmDetails.InstanceMetadata.HTTPPutResponseHopLimit) + } + if occmDetails.InstanceMetadata.HTTPTokens != nil { + runInstancesInput.MetadataOptions.HttpTokens = aws.String(*occmDetails.InstanceMetadata.HTTPTokens) + } log.Print("CallAWSInstanceCreate occmDetails name:", occmDetails.Name) runResult, err := svc.RunInstances(runInstancesInput) @@ -173,6 +183,42 @@ func (c *Client) CallAWSInstanceCreate(occmDetails createOCCMDetails) (string, e return *runResult.Instances[0].InstanceId, nil } +// CallAWSInstanceUpdate updates the instance metadata +func (c *Client) CallAWSInstanceUpdate(occmDetails createAWSOCCMDetails) error { + + var sess *session.Session + if c.AWSProfile != "" { + sess = session.Must(session.NewSession( + &aws.Config{ + Region: aws.String(occmDetails.Region), + Credentials: credentials.NewSharedCredentials(c.AWSProfileFilePath, c.AWSProfile), + })) + } else { + sess = session.Must(session.NewSession(aws.NewConfig().WithRegion(occmDetails.Region))) + } + + // Create EC2 service client + svc := ec2.New(sess) + + modifyInstanceMetadataInput := &ec2.ModifyInstanceMetadataOptionsInput{ + InstanceId: aws.String(occmDetails.InstanceID), + HttpTokens: aws.String(*occmDetails.InstanceMetadata.HTTPTokens), + HttpEndpoint: aws.String(*occmDetails.InstanceMetadata.HTTPEndpoint), + HttpPutResponseHopLimit: aws.Int64(*occmDetails.InstanceMetadata.HTTPPutResponseHopLimit), + } + + log.Print("CallAWSInstanceUpdate occmDetails name:", occmDetails.Name) + result, err := svc.ModifyInstanceMetadataOptions(modifyInstanceMetadataInput) + + if err != nil { + log.Print("Could not update instance ", err) + return err + } + + log.Printf("Updated instance %s", *result.InstanceMetadataOptions, *result.InstanceId) + return nil +} + // CallAWSInstanceTerminate can be used to make a request to terminate AWS Instance func (c *Client) CallAWSInstanceTerminate(occmDetails deleteOCCMDetails) error { @@ -524,7 +570,7 @@ func (c *Client) CallDeleteAzureVM(occmDetails deleteOCCMDetails) error { } // CallAMIGet can be used to make a request to get AWS AMI -func (c *Client) CallAMIGet(occmDetails createOCCMDetails) (string, error) { +func (c *Client) CallAMIGet(occmDetails createAWSOCCMDetails) (string, error) { var sess *session.Session if c.AWSProfile != "" { @@ -653,7 +699,7 @@ func (c *Client) CallVNetGetCidr(subscriptionID string, resourceGroup string, vn } // CallAWSInstanceGet can be used to make a request to get AWS Instance -func (c *Client) CallAWSInstanceGet(occmDetails createOCCMDetails) ([]ec2.Instance, error) { +func (c *Client) CallAWSInstanceGet(occmDetails createAWSOCCMDetails) ([]ec2.Instance, error) { if occmDetails.Region == "" { regions, err := c.CallAWSRegionGet(occmDetails) if err != nil { @@ -733,7 +779,7 @@ func (c *Client) CallAWSInstanceGet(occmDetails createOCCMDetails) ([]ec2.Instan } // CallAWSRegionGet describe all regions. -func (c *Client) CallAWSRegionGet(occmDetails createOCCMDetails) ([]string, error) { +func (c *Client) CallAWSRegionGet(occmDetails createAWSOCCMDetails) ([]string, error) { var sess *session.Session if c.AWSProfile != "" { sess = session.Must(session.NewSession( @@ -854,7 +900,7 @@ func (c *Client) GetSimulator() bool { } // CallAWSTagCreate creates tag -func (c *Client) CallAWSTagCreate(occmDetails createOCCMDetails) error { +func (c *Client) CallAWSTagCreate(occmDetails createAWSOCCMDetails) error { var sess *session.Session if c.AWSProfile != "" { @@ -900,7 +946,7 @@ func (c *Client) CallAWSTagCreate(occmDetails createOCCMDetails) error { } // CallAWSTagDelete deletes tag -func (c *Client) CallAWSTagDelete(occmDetails createOCCMDetails) error { +func (c *Client) CallAWSTagDelete(occmDetails createAWSOCCMDetails) error { var sess *session.Session if c.AWSProfile != "" { @@ -946,7 +992,7 @@ func (c *Client) CallAWSTagDelete(occmDetails createOCCMDetails) error { } // CallAWSDescribeInstanceAttribute returns disableAPITermination. -func (c *Client) CallAWSDescribeInstanceAttribute(occmDetails createOCCMDetails) (bool, error) { +func (c *Client) CallAWSDescribeInstanceAttribute(occmDetails createAWSOCCMDetails) (bool, error) { var sess *session.Session if c.AWSProfile != "" { diff --git a/cloudmanager/occm_aws.go b/cloudmanager/occm_aws.go index f85eb10..ec0b718 100644 --- a/cloudmanager/occm_aws.go +++ b/cloudmanager/occm_aws.go @@ -70,6 +70,56 @@ type createOCCMDetails struct { Labels map[string]string } +// Each Connector should have its own struct as each cloud provider has different fields +// createAWSOCCMDetails the users input for creating a occm +type createAWSOCCMDetails struct { + Name string + GCPProject string + Company string + InstanceID string + Region string + Location string + Zone string + AMI string + KeyName string + InstanceType string + IamInstanceProfileName string + SecurityGroupID string + SubnetID string + NetworkProjectID string + ProxyURL string + ProxyUserName string + ProxyPassword string + ResourceGroup string + SubscriptionID string + MachineType string + ServiceAccountEmail string + GCPCommonSuffixName string + VnetID string + VnetResourceGroup string + AdminUsername string + AdminPassword string + VirtualMachineSize string + NetworkSecurityGroupName string + NetworkSecurityResourceGroup string + AssociatePublicIPAddress *bool + AssociatePublicIP bool + FirewallTags bool + EnableTerminationProtection *bool + AwsTags []userTags + Tags []string + StorageAccount string + Labels map[string]string + InstanceMetadata AWSInstanceMetadata +} + +// AWSInstanceMetadata describes the metadata options for the ec2 instance +type AWSInstanceMetadata struct { + HTTPEndpoint *string + HTTPPutResponseHopLimit *int64 + HTTPTokens *string +} + // deleteOCCMDetails the users input for deleting a occm type deleteOCCMDetails struct { InstanceID string @@ -338,7 +388,7 @@ func (c *Client) createAccount(clientID string) (string, error) { return result.AccountID, nil } -func (c *Client) createAWSInstance(occmDetails createOCCMDetails, clientID string) (string, error) { +func (c *Client) createAWSInstance(occmDetails createAWSOCCMDetails, clientID string) (string, error) { instanceID, err := c.CallAWSInstanceCreate(occmDetails) if err != nil { @@ -369,12 +419,12 @@ func (c *Client) createAWSInstance(occmDetails createOCCMDetails, clientID strin return instanceID, nil } -func (c *Client) getAWSInstance(occmDetails createOCCMDetails, id string) (ec2.Instance, error) { +func (c *Client) getAWSInstance(occmDetails createAWSOCCMDetails, id string) (ec2.Instance, error) { log.Print("getAWSInstance") res, err := c.CallAWSInstanceGet(occmDetails) - returnOCCM := createOCCMDetails{} + returnOCCM := createAWSOCCMDetails{} if err != nil { return ec2.Instance{}, err } @@ -384,13 +434,65 @@ func (c *Client) getAWSInstance(occmDetails createOCCMDetails, id string) (ec2.I returnOCCM.AMI = *instance.ImageId returnOCCM.InstanceID = *instance.InstanceId returnOCCM.InstanceType = *instance.InstanceType + returnOCCM.InstanceMetadata = AWSInstanceMetadata{ + HTTPEndpoint: instance.MetadataOptions.HttpEndpoint, + HTTPPutResponseHopLimit: instance.MetadataOptions.HttpPutResponseHopLimit, + HTTPTokens: instance.MetadataOptions.HttpTokens, + } return instance, nil } } return ec2.Instance{}, nil } -func (c *Client) createOCCM(occmDetails createOCCMDetails, proxyCertificates []string, clientID string) (OCCMMResult, error) { +// TODO: move this general function out of this file, As it is sepefic to AWS +func (c *Client) createOCCM(occmDetails createAWSOCCMDetails, proxyCertificates []string, clientID string) (OCCMMResult, error) { + log.Printf("createOCCM %s %s", occmDetails.Name, clientID) + if occmDetails.AMI == "" { + + ami, err := c.CallAMIGet(occmDetails) + if err != nil { + return OCCMMResult{}, err + } + occmDetails.AMI = ami + } + + var registerAgentTOService registerAgentTOServiceRequest + registerAgentTOService.Name = occmDetails.Name + registerAgentTOService.Placement.Region = occmDetails.Region + registerAgentTOService.Placement.Subnet = occmDetails.SubnetID + registerAgentTOService.Company = occmDetails.Company + if occmDetails.ProxyURL != "" { + registerAgentTOService.Extra.Proxy.ProxyURL = occmDetails.ProxyURL + } + + if occmDetails.ProxyUserName != "" { + registerAgentTOService.Extra.Proxy.ProxyUserName = occmDetails.ProxyUserName + } + + if occmDetails.ProxyPassword != "" { + registerAgentTOService.Extra.Proxy.ProxyPassword = occmDetails.ProxyPassword + } + + userData, newClientID, err := c.getUserData(registerAgentTOService, proxyCertificates, clientID) + if err != nil { + return OCCMMResult{}, err + } + c.UserData = userData + var result OCCMMResult + result.ClientID = newClientID + result.AccountID = c.AccountID + instanceID, err := c.createAWSInstance(occmDetails, newClientID) + if err != nil { + return OCCMMResult{}, err + } + result.InstanceID = instanceID + + log.Printf("createOCCM clientID: %s, cclient=%s", result.ClientID, newClientID) + return result, nil +} + +func (c *Client) createAWSOCCM(occmDetails createAWSOCCMDetails, proxyCertificates []string, clientID string) (OCCMMResult, error) { log.Printf("createOCCM %s %s", occmDetails.Name, clientID) if occmDetails.AMI == "" { @@ -522,7 +624,7 @@ func (c *Client) deleteOCCM(request deleteOCCMDetails, clientID string) error { } // only tags can be updated. Other update functionalities to be added. -func (c *Client) updateOCCM(occmDetails createOCCMDetails, proxyCertificates []string, deleteTags []userTags, addModifyTags []userTags, clientID string) error { +func (c *Client) updateOCCM(occmDetails createAWSOCCMDetails, proxyCertificates []string, deleteTags []userTags, addModifyTags []userTags, clientID string, callAWSInstanceUpdate bool) error { log.Print("updating OCCM") if occmDetails.AMI == "" { @@ -533,39 +635,48 @@ func (c *Client) updateOCCM(occmDetails createOCCMDetails, proxyCertificates []s } occmDetails.AMI = ami } + //No documentation on the follwing code. It was working until the time instance update was added. The error is: + // code: 403, message: Action not allowed for user - var registerAgentTOService registerAgentTOServiceRequest - registerAgentTOService.Name = occmDetails.Name - registerAgentTOService.Placement.Region = occmDetails.Region - registerAgentTOService.Placement.Subnet = occmDetails.SubnetID - registerAgentTOService.Company = occmDetails.Company - if occmDetails.ProxyURL != "" { - registerAgentTOService.Extra.Proxy.ProxyURL = occmDetails.ProxyURL - } + // var registerAgentTOService registerAgentTOServiceRequest + // registerAgentTOService.Name = occmDetails.Name + // registerAgentTOService.Placement.Region = occmDetails.Region + // registerAgentTOService.Placement.Subnet = occmDetails.SubnetID + // registerAgentTOService.Company = occmDetails.Company + // if occmDetails.ProxyURL != "" { + // registerAgentTOService.Extra.Proxy.ProxyURL = occmDetails.ProxyURL + // } - if occmDetails.ProxyUserName != "" { - registerAgentTOService.Extra.Proxy.ProxyUserName = occmDetails.ProxyUserName - } + // if occmDetails.ProxyUserName != "" { + // registerAgentTOService.Extra.Proxy.ProxyUserName = occmDetails.ProxyUserName + // } - if occmDetails.ProxyPassword != "" { - registerAgentTOService.Extra.Proxy.ProxyPassword = occmDetails.ProxyPassword - } + // if occmDetails.ProxyPassword != "" { + // registerAgentTOService.Extra.Proxy.ProxyPassword = occmDetails.ProxyPassword + // } + + // userData, _, err := c.getUserData(registerAgentTOService, proxyCertificates, clientID) + // if err != nil { + // return err + // } + // c.UserData = userData - userData, _, err := c.getUserData(registerAgentTOService, proxyCertificates, clientID) - if err != nil { - return err - } - c.UserData = userData if len(addModifyTags) > 0 { occmDetails.AwsTags = addModifyTags - err = c.CallAWSTagCreate(occmDetails) + err := c.CallAWSTagCreate(occmDetails) if err != nil { return err } } if len(deleteTags) > 0 { occmDetails.AwsTags = deleteTags - err = c.CallAWSTagDelete(occmDetails) + err := c.CallAWSTagDelete(occmDetails) + if err != nil { + return err + } + } + if callAWSInstanceUpdate { + err := c.CallAWSInstanceUpdate(occmDetails) if err != nil { return err } diff --git a/cloudmanager/resource_netapp_cloudmanager_connector_aws.go b/cloudmanager/resource_netapp_cloudmanager_connector_aws.go index 42b568d..aaf686f 100644 --- a/cloudmanager/resource_netapp_cloudmanager_connector_aws.go +++ b/cloudmanager/resource_netapp_cloudmanager_connector_aws.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/hashicorp/terraform/helper/schema" + "github.com/hashicorp/terraform/helper/validation" ) func resourceOCCMAWS() *schema.Resource { @@ -143,6 +144,33 @@ func resourceOCCMAWS() *schema.Resource { Optional: true, Computed: true, }, + "instance_metadata": { + Type: schema.TypeSet, + Optional: true, + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "http_put_response_hop_limit": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ValidateFunc: validation.IntBetween(1, 64), + }, + "http_tokens": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"optional", "required"}, false), + }, + "http_endpoint": { + Type: schema.TypeString, + Optional: true, + Computed: true, + ValidateFunc: validation.StringInSlice([]string{"enabled", "disabled"}, false), + }, + }, + }, + }, }, } } @@ -152,7 +180,7 @@ func resourceOCCMAWSCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*Client) - occmDetails := createOCCMDetails{} + occmDetails := createAWSOCCMDetails{} occmDetails.Name = d.Get("name").(string) occmDetails.Region = d.Get("region").(string) @@ -222,7 +250,24 @@ func resourceOCCMAWSCreate(d *schema.ResourceData, meta interface{}) error { } } - res, err := client.createOCCM(occmDetails, proxyCertificates, "") + if o, ok := d.GetOk("instance_metadata"); ok { + instanceMetaData := o.(*schema.Set) + if instanceMetaData.Len() == 1 { + instanceMetadata := instanceMetaData.List()[0].(map[string]interface{}) + httpPutResponseHopLimit := int64(instanceMetadata["http_put_response_hop_limit"].(int)) + httpTokens := instanceMetadata["http_tokens"].(string) + httpEndpoint := instanceMetadata["http_endpoint"].(string) + occmDetails.InstanceMetadata = AWSInstanceMetadata{ + HTTPPutResponseHopLimit: &httpPutResponseHopLimit, + HTTPTokens: &httpTokens, + HTTPEndpoint: &httpEndpoint, + } + } else { + return fmt.Errorf("error creating occm: the length of instance_metadata can not be more than 1. Current length: %d", instanceMetaData.Len()) + } + } + + res, err := client.createAWSOCCM(occmDetails, proxyCertificates, "") if err != nil { log.Print("Error creating instance") @@ -247,7 +292,7 @@ func resourceOCCMAWSRead(d *schema.ResourceData, meta interface{}) error { log.Printf("Reading OCCM: %#v", d) client := meta.(*Client) var clientID string - occmDetails := createOCCMDetails{} + occmDetails := createAWSOCCMDetails{} occmDetails.Name = d.Get("name").(string) occmDetails.Region = d.Get("region").(string) @@ -282,6 +327,14 @@ func resourceOCCMAWSRead(d *schema.ResourceData, meta interface{}) error { return fmt.Errorf("expected occm ID %s, Response could not find", id) } + instanceMetadata := make([]map[string]interface{}, 0) + instanceMetadataMap := make(map[string]interface{}) + instanceMetadataMap["http_put_response_hop_limit"] = *res.MetadataOptions.HttpPutResponseHopLimit + instanceMetadataMap["http_tokens"] = *res.MetadataOptions.HttpTokens + instanceMetadataMap["http_endpoint"] = *res.MetadataOptions.HttpEndpoint + instanceMetadata = append(instanceMetadata, instanceMetadataMap) + d.Set("instance_metadata", instanceMetadata) + if occmDetails.Region == "" { occmDetails.Region = *res.Placement.AvailabilityZone occmDetails.Region = occmDetails.Region[:len(occmDetails.Region)-1] @@ -391,7 +444,7 @@ func resourceOCCMAWSExists(d *schema.ResourceData, meta interface{}) (bool, erro client := meta.(*Client) id := d.Id() - occmDetails := createOCCMDetails{} + occmDetails := createAWSOCCMDetails{} occmDetails.Name = d.Get("name").(string) occmDetails.Region = d.Get("region").(string) @@ -429,7 +482,7 @@ func resourceOCCMAWSExists(d *schema.ResourceData, meta interface{}) (bool, erro func resourceOCCMAWSUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*Client) - occmDetails := createOCCMDetails{} + occmDetails := createAWSOCCMDetails{} deleteAwsTags := []userTags{} addModifyAwsTags := []userTags{} if d.HasChange("aws_tag") { @@ -458,8 +511,26 @@ func resourceOCCMAWSUpdate(d *schema.ResourceData, meta interface{}) error { occmDetails.IamInstanceProfileName = d.Get("iam_instance_profile_name").(string) occmDetails.Company = d.Get("company").(string) occmDetails.InstanceID = d.Id() + callAWSInstanceUpdate := false + if d.HasChange("instance_metadata") { + callAWSInstanceUpdate = true + instanceMetaData := d.Get("instance_metadata").(*schema.Set) + if instanceMetaData.Len() == 1 { + instanceMetadata := instanceMetaData.List()[0].(map[string]interface{}) + httpPutResponseHopLimit := int64(instanceMetadata["http_put_response_hop_limit"].(int)) + httpTokens := instanceMetadata["http_tokens"].(string) + httpEndpoint := instanceMetadata["http_endpoint"].(string) + occmDetails.InstanceMetadata = AWSInstanceMetadata{ + HTTPPutResponseHopLimit: &httpPutResponseHopLimit, + HTTPTokens: &httpTokens, + HTTPEndpoint: &httpEndpoint, + } + } else { + return fmt.Errorf("error updataing occm: the length of instance_metadata can not be more than 1. Current length: %d", instanceMetaData.Len()) + } + } - err := client.updateOCCM(occmDetails, nil, deleteAwsTags, addModifyAwsTags, clientID) + err := client.updateOCCM(occmDetails, nil, deleteAwsTags, addModifyAwsTags, clientID, callAWSInstanceUpdate) if err != nil { log.Print("Error updating instance") diff --git a/website/docs/r/connector_aws.html.markdown b/website/docs/r/connector_aws.html.markdown index 3a47b77..8edf696 100644 --- a/website/docs/r/connector_aws.html.markdown +++ b/website/docs/r/connector_aws.html.markdown @@ -36,6 +36,11 @@ resource "netapp-cloudmanager_connector_aws" "cl-occm-aws" { tag_key = "xxx" tag_value = "YYY" } + instance_metadata{ + http_put_response_hop_limit = 2 + http_tokens = "required" + http_endpoint = "enabled" + } subnet_id = "subnet-xxxxx" security_group_id = "sg-xxxxxxxxx" iam_instance_profile_name = "OCCM_AUTOMATION" @@ -59,16 +64,20 @@ The following arguments are supported: * `proxy_user_name` - (Optional) The proxy user name, if using a proxy to connect to the internet. * `proxy_password` - (Optional) The proxy password, if using a proxy to connect to the internet. * `proxy_certificates` - (Optional) The proxy certificates. A list of certificate file names. - -Options * `associate_public_ip_address` - (Optional) Indicates whether to associate a public IP address to the instance. If not provided, the association will be done based on the subnet's configuration. * `enable_termination_protection` - (Optional) Indicates whether to enable termination protection on the instance, default is false. * `account_id` - (Optional) The NetApp account ID that the Connector will be associated with. If not provided, Cloud Manager uses the first account. If no account exists, Cloud Manager creates a new account. You can find the account ID in the account tab of Cloud Manager at [https://console.bluexp.netapp.com/](https://console.bluexp.netapp.com/). +* `instance_metadata` - (Optional,Computed) The block of AWS EC2 instacne metadata. The `aws_tag` block supports the following: * `tag_key` - (Required) The key of the tag. * `tag_value` - (Required) The tag value. +The `instance_metadata` block supports the following: +* `http_endpoint` - (Optional, Computed) If the value is disabled, you cannot access your instance metadata. Choices: ["enabled", "disabled"] +* `http_tokens` - (Optional, Computed) Indicates whether IMDSv2 is required. Choices: ["optional", "required"] +* `http_put_response_hop_limit` - (Optional, Computed) The desired HTTP PUT response hop limit for instance metadata requests. The larger the number, the further instance metadata requests can travel. Possible values: Integers from 1 to 64. + ## Attributes Reference The following attributes are exported in addition to the arguments listed above: