From 204cd2b44792bcbbc5590d8b8e90341144909981 Mon Sep 17 00:00:00 2001 From: Kobi Samoray Date: Mon, 25 Nov 2024 11:52:30 +0200 Subject: [PATCH] Add support for Edge transport nodes which were created externally Normally users create Edge Transport Nodes within NSX, which deploys them into the regsitered compute manager. However, users have the option to do the same by deploying the Edge appliance anywhere, outside NSX, and registering it with NSX using the Edge CLI or OVA parameters. Later, this Edge appliance can be converted into a transport node using NSX API - this change attempts to utilize this capability. Fixes: #1459 Signed-off-by: Kobi Samoray --- nsxt/resource_nsxt_edge_transport_node.go | 136 +++++++++++++----- .../transport_node_realization.html.markdown | 2 +- .../docs/r/edge_transport_node.html.markdown | 35 ++++- 3 files changed, 133 insertions(+), 40 deletions(-) diff --git a/nsxt/resource_nsxt_edge_transport_node.go b/nsxt/resource_nsxt_edge_transport_node.go index ad7f9aace..cdaa975f0 100644 --- a/nsxt/resource_nsxt_edge_transport_node.go +++ b/nsxt/resource_nsxt_edge_transport_node.go @@ -97,6 +97,13 @@ func resourceNsxtEdgeTransportNode() *schema.Resource { "description": getDescriptionSchema(), "display_name": getDisplayNameSchema(), "tag": getTagsSchema(), + "node_id": { + Type: schema.TypeString, + Optional: true, + Computed: true, + Description: "Unique Id of the fabric node", + ConflictsWith: []string{"ip_addresses", "fqdn", "deployment_config", "external_id"}, + }, "failure_domain": { Type: schema.TypeString, Optional: true, @@ -671,43 +678,56 @@ func getTransportNodeFromSchema(d *schema.ResourceData, m interface{}) (*mpmodel description := d.Get("description").(string) displayName := d.Get("display_name").(string) tags := getMPTagsFromSchema(d) + nodeID := d.Get("node_id").(string) failureDomain := d.Get("failure_domain").(string) hostSwitchSpec, err := getHostSwitchSpecFromSchema(d, m, nodeTypeEdge) if err != nil { return nil, fmt.Errorf("failed to create Transport Node: %v", err) } - converter := bindings.NewTypeConverter() - var dataValue data.DataValue - var errs []error + var nodeDeploymentInfo *data.StructValue + if nodeID == "" { + /* + node_id attribute conflicts with node_deployment_info. As node_deployment_info is a complex object which has + attributes with default values, this schema property will always have values - therefore there is no simple way + to enforce this conflict within the provider (e.g check if node_id and node_deployment_info have values, then + fail. + So the provider will ignore node_deployment_info properties when node_id has a value - which would mean that + this edge appliance was created externally. + */ + log.Printf("node_id not specified, will deploy edge using values in deploymentConfig") + converter := bindings.NewTypeConverter() + var dataValue data.DataValue + var errs []error - externalID := d.Get("external_id").(string) - fqdn := d.Get("fqdn").(string) - ipAddresses := interfaceListToStringList(d.Get("ip_addresses").([]interface{})) + externalID := d.Get("external_id").(string) + fqdn := d.Get("fqdn").(string) + ipAddresses := interfaceListToStringList(d.Get("ip_addresses").([]interface{})) - deploymentConfig, err := getEdgeNodeDeploymentConfigFromSchema(d.Get("deployment_config")) - if err != nil { - return nil, err - } - nodeSettings, err := getEdgeNodeSettingsFromSchema(d.Get("node_settings")) - if err != nil { - return nil, err - } - node := mpmodel.EdgeNode{ - ExternalId: &externalID, - Fqdn: &fqdn, - IpAddresses: ipAddresses, - DeploymentConfig: deploymentConfig, - NodeSettings: nodeSettings, - ResourceType: mpmodel.EdgeNode__TYPE_IDENTIFIER, - } - dataValue, errs = converter.ConvertToVapi(node, mpmodel.EdgeNodeBindingType()) + deploymentConfig, err := getEdgeNodeDeploymentConfigFromSchema(d.Get("deployment_config")) + if err != nil { + return nil, err + } + nodeSettings, err := getEdgeNodeSettingsFromSchema(d.Get("node_settings")) + if err != nil { + return nil, err + } + node := mpmodel.EdgeNode{ + ExternalId: &externalID, + Fqdn: &fqdn, + IpAddresses: ipAddresses, + DeploymentConfig: deploymentConfig, + NodeSettings: nodeSettings, + ResourceType: mpmodel.EdgeNode__TYPE_IDENTIFIER, + } + dataValue, errs = converter.ConvertToVapi(node, mpmodel.EdgeNodeBindingType()) - if errs != nil { - log.Printf("Failed to convert node object, errors are %v", errs) - return nil, errs[0] + if errs != nil { + log.Printf("Failed to convert node object, errors are %v", errs) + return nil, errs[0] + } + nodeDeploymentInfo = dataValue.(*data.StructValue) } - nodeDeploymentInfo := dataValue.(*data.StructValue) obj := mpmodel.TransportNode{ Description: &description, @@ -725,6 +745,37 @@ func resourceNsxtEdgeTransportNodeCreate(d *schema.ResourceData, m interface{}) connector := getPolicyConnector(m) client := nsx.NewTransportNodesClient(connector) + nodeID := d.Get("node_id").(string) + if nodeID != "" { + obj, err := client.Get(nodeID) + if err != nil { + return handleCreateError("TransportNode", nodeID, err) + } + // Set node_id, revision and computed values in schema + d.Set("failure_domain", obj.FailureDomainId) + + converter := bindings.NewTypeConverter() + base, errs := converter.ConvertToGolang(obj.NodeDeploymentInfo, mpmodel.EdgeNodeBindingType()) + if errs != nil { + return handleCreateError("TransportNode", nodeID, errs[0]) + } + node := base.(mpmodel.EdgeNode) + d.Set("external_id", node.ExternalId) + d.Set("fqdn", node.Fqdn) + d.Set("ip_addresses", node.IpAddresses) + if obj.Revision != nil { + d.Set("revision", obj.Revision) + } + + d.SetId(nodeID) + err = resourceNsxtEdgeTransportNodeUpdate(d, m) + if err != nil { + // There is a failure in update, let's discard this so state will remain clean + d.SetId("") + } + return err + } + obj, err := getTransportNodeFromSchema(d, m) if err != nil { return err @@ -914,23 +965,31 @@ func getCPUConfigFromSchema(cpuConfigList []interface{}) []mpmodel.CpuCoreConfig func getHostSwitchProfileResourceType(m interface{}, id string) (string, error) { connector := getPolicyConnector(m) client := infra.NewHostSwitchProfilesClient(connector) - structValue, err := client.Get(id) + // we retrieve a list of profiles instead of using Get(), as the id could be either MP id or Policy id + list, err := client.List(nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil) if err != nil { return "", err } - converter := bindings.NewTypeConverter() - baseInterface, errs := converter.ConvertToGolang(structValue, model.PolicyBaseHostSwitchProfileBindingType()) - if errs != nil { - return "", errs[0] - } - base := baseInterface.(model.PolicyBaseHostSwitchProfile) - resourceType, ok := mpHostSwitchProfileTypeFromPolicyType[base.ResourceType] - if !ok { - return "", fmt.Errorf("MP resource type not found for %s", base.ResourceType) + + for _, structValue := range list.Results { + baseInterface, errs := converter.ConvertToGolang(structValue, model.PolicyBaseHostSwitchProfileBindingType()) + if errs != nil { + return "", errs[0] + } + base := baseInterface.(model.PolicyBaseHostSwitchProfile) + + if *base.Id == id || *base.RealizationId == id { + resourceType, ok := mpHostSwitchProfileTypeFromPolicyType[base.ResourceType] + if !ok { + return "", fmt.Errorf("MP resource type not found for %s", base.ResourceType) + } + return resourceType, nil + } } - return resourceType, nil + + return "", fmt.Errorf("Host Switch Profile type not found for %s", id) } func getHostSwitchProfileIDsFromSchema(m interface{}, hswProfileList []interface{}) ([]mpmodel.HostSwitchProfileTypeIdEntry, error) { @@ -1236,6 +1295,7 @@ func resourceNsxtEdgeTransportNodeRead(d *schema.ResourceData, m interface{}) er d.Set("description", obj.Description) d.Set("display_name", obj.DisplayName) setMPTagsInSchema(d, obj.Tags) + d.Set("node_id", obj.NodeId) d.Set("failure_domain", obj.FailureDomainId) if obj.HostSwitchSpec != nil { diff --git a/website/docs/d/transport_node_realization.html.markdown b/website/docs/d/transport_node_realization.html.markdown index 9785be490..32322f526 100644 --- a/website/docs/d/transport_node_realization.html.markdown +++ b/website/docs/d/transport_node_realization.html.markdown @@ -45,7 +45,7 @@ resource "nsxt_transport_node" "test" { } } node_settings { - hostname = "tf_edge_node" + hostname = "tf-edge-node" allow_ssh_root_login = true enable_ssh = true } diff --git a/website/docs/r/edge_transport_node.html.markdown b/website/docs/r/edge_transport_node.html.markdown index b462feb09..cf0d8b043 100644 --- a/website/docs/r/edge_transport_node.html.markdown +++ b/website/docs/r/edge_transport_node.html.markdown @@ -46,7 +46,7 @@ resource "nsxt_edge_transport_node" "test" { } } node_settings { - hostname = "tf_edge_node" + hostname = "tf-edge-node" allow_ssh_root_login = true enable_ssh = true } @@ -56,6 +56,38 @@ resource "nsxt_edge_transport_node" "test" { **NOTE:** `data.vsphere_network`, `data.vsphere_compute_cluster`, `data.vsphere_datastore`, `data.vsphere_host` are obtained using [hashicorp/vsphere](https://registry.terraform.io/providers/hashicorp/vsphere/) provider. +## Example Usage, with Edge Transport Node created externally and converted into a transport node using Terraform +```hcl +data "nsxt_transport_node" "test_node" { + display_name = "tf_edge_node" +} + +resource "nsxt_edge_transport_node" "test_node" { + node_id = data.nsxt_transport_node.test_node.id + description = "Terraform-deployed edge node" + display_name = "tf_edge_node" + standard_host_switch { + ip_assignment { + static_ip_pool = data.nsxt_policy_ip_pool.vtep_ip_pool.realized_id + } + transport_zone_endpoint { + transport_zone = data.nsxt_transport_zone.overlay_tz.id + } + transport_zone_endpoint { + transport_zone = data.nsxt_transport_zone.vlan_tz.id + } + host_switch_profile = [data.nsxt_policy_uplink_host_switch_profile.edge_uplink_profile.path] + pnic { + device_name = "fp-eth0" + uplink_name = "uplink1" + } + } + node_settings { + hostname = "tf-edge-node" + } +} +``` + ## Argument Reference The following arguments are supported: @@ -63,6 +95,7 @@ The following arguments are supported: * `display_name` - (Required) Display name of the resource. * `description` - (Optional) Description of the resource. * `tag` - (Optional) A list of scope + tag pairs to associate with this resource. +* `node_id` - (Optional) The id of a pre-deployed Edge appliance to be converted into a transport node. Note that `node_id` attribute conflicts with `external_id`, `fqdn`, `ip_addresses` `deployment_config` and `node_settings` and those will be ignored while specifying `node_id`. * `failure_domain` - (Optional) Id of the failure domain. * `standard_host_switch` - (Required) Standard host switch specification. * `host_switch_id` - (Optional) The host switch id. This ID will be used to reference a host switch.