diff --git a/docs/data-sources/fabric_precision_time.md b/docs/data-sources/fabric_precision_time.md new file mode 100644 index 000000000..dec0f8660 --- /dev/null +++ b/docs/data-sources/fabric_precision_time.md @@ -0,0 +1,133 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "equinix_fabric_precision_time Data Source - terraform-provider-equinix" +subcategory: "Fabric" +description: |- + Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Precision Time Service by Uuid +--- + +# equinix_fabric_precision_time (Data Source) + +Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Precision Time Service by Uuid + +Additional documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Edge-Services/EPT/EPT.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#precision-time + +## Example Usage + +```terraform +data "equinix_fabric_precision_time" "time_service" { + uuid = "f9361b9a-a0b3-4fa3-bf36-be265dc7617f" +} +``` + + + +## Schema + +### Required + +- `uuid` (String) Uuid of Precision Time Service resource; used for lookup + +### Read-Only + +- `account` (List of Object) Equinix User Account associated with Precision Time Service (see [below for nested schema](#nestedatt--account)) +- `advance_configuration` (List of Object) An object that has advanced configuration options. (see [below for nested schema](#nestedatt--advance_configuration)) +- `connections` (Block List) An array of objects with unique identifiers of connections. (see [below for nested schema](#nestedblock--connections)) +- `description` (String) Optional description of time service +- `href` (String) Equinix generated Portal link for the created Precision Time Service +- `id` (String) The unique identifier of the resource +- `ipv4` (Block, Read-only) An object that has Network IP Configurations for Timing Master Servers. (see [below for nested schema](#nestedblock--ipv4)) +- `name` (String) Name of Precision Time Service. Applicable values: Maximum: 24 characters; Allowed characters: alpha-numeric, hyphens ('-') and underscores ('_') +- `package` (Block, Read-only) Precision Time Service Package Details (see [below for nested schema](#nestedblock--package)) +- `project_id` (String) Equinix Fabric Project ID +- `state` (String) Indicator of the state of this Precision Time Service. One of: [[PROVISIONED PROVISIONING PROVISIONING_FAILED CONFIGURING CANCELLED DEPROVISIONING_FAILED PENDING_CONFIGURATION DEPROVISIONED CONFIGURING_FAILED DEPROVISIONING]] +- `type` (String) Choose type of Precision Time Service + + +### Nested Schema for `account` + +Read-Only: + +- `account_number` (Number) +- `global_org_id` (String) +- `is_reseller_account` (Boolean) +- `org_id` (String) + + + +### Nested Schema for `advance_configuration` + +Read-Only: + +- `ntp` (List of Object) (see [below for nested schema](#nestedobjatt--advance_configuration--ntp)) +- `ptp` (List of Object) (see [below for nested schema](#nestedobjatt--advance_configuration--ptp)) + + +### Nested Schema for `advance_configuration.ntp` + +Read-Only: + +- `id` (String) +- `password` (String) +- `type` (String) + + + +### Nested Schema for `advance_configuration.ptp` + +Read-Only: + +- `domain` (Number) +- `grant_time` (Number) +- `log_announce_interval` (Number) +- `log_delay_req_interval` (Number) +- `log_sync_interval` (Number) +- `priority_1` (Number) +- `priority_2` (Number) +- `time_scale` (String) +- `transport_mode` (String) + + + + +### Nested Schema for `connections` + +Read-Only: + +- `href` (String) Link to the Equinix Fabric Connection associated with the Precision Time Service +- `type` (String) Type of the Equinix Fabric Connection associated with the Precision Time Service +- `uuid` (String) Equinix Fabric Connection UUID; Precision Time Service will be connected with it + + + +### Nested Schema for `ipv4` + +Read-Only: + +- `default_gateway` (String) IPv4 address that establishes the Routing Interface where traffic is directed. It serves as the next hop in the Network. +- `network_mask` (String) IPv4 address that defines the range of consecutive subnets in the network. +- `primary` (String) IPv4 address for the Primary Timing Master Server. +- `secondary` (String) IPv4 address for the Secondary Timing Master Server. + + + +### Nested Schema for `package` + +Read-Only: + +- `accuracy_avg_max` (Number) Average maximum accuracy provided by the Precision Time Service +- `accuracy_avg_min` (Number) Average minimum accuracy provided by the Precision Time Service +- `accuracy_sla` (Number) SLA for the accuracy provided by the Precision Time Service +- `accuracy_unit` (String) Time unit of accuracy for the Precision Time Service; e.g. microseconds +- `bandwidth` (Number) Bandwidth of the Precision Time Service +- `clients_per_second_max` (Number) Maximum clients available per second for the Precision Time Service +- `code` (String) Time Precision Package Code for the desired billing package +- `href` (String) Time Precision Package HREF link to corresponding resource in Equinix Portal +- `multi_subnet_supported` (Boolean) Boolean flag indicating if this Precision Time Service supports multi subnetting +- `redundancy_supported` (Boolean) Boolean flag indicating if this Precision Time Service supports redundancy +- `type` (String) Type of the Precision Time Service Package + + + diff --git a/docs/resources/fabric_precision_time.md b/docs/resources/fabric_precision_time.md new file mode 100644 index 000000000..71e1441ee --- /dev/null +++ b/docs/resources/fabric_precision_time.md @@ -0,0 +1,192 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "equinix_fabric_precision_time Resource - terraform-provider-equinix" +subcategory: "Fabric" +description: |- + Fabric V4 API compatible resource allows creation and management of Equinix Fabric Precision Time +--- + +# equinix_fabric_precision_time (Resource) + +Fabric V4 API compatible resource allows creation and management of Equinix Fabric Precision Time Services + +Additional documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Edge-Services/EPT/EPT.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#precision-time + +## Example Usage + +PTP Configuration: +```terraform +resource "equinix_fabric_precision_time" "ptp" { + type = "PTP" + name = "tf_ept_PFCR" + description = "Equinix Precision Time with PTP Configuration" + package { + code = "PTP_STANDARD" + } + connections { + uuid = "30b82c65-ffb4-47d3-ab2b-3cacf46d5b8b" + } + ipv4 { + primary = "192.168.0.2" + secondary = "192.168.0.3" + network_mask = "255.255.255.224" + default_gateway = "192.168.0.1" + } +} +``` + +NTP Configuration: +```terraform +resource "equinix_fabric_precision_time" "ntp" { + type = "NTP" + name = "tf_ntp_PFCR" + description = "Equinix Precision Time with NTP Configuration" + package { + code = "NTP_STANDARD" + } + connections { + uuid = "30b82c65-ffb4-47d3-ab2b-3cacf46d5b8b" + } + ipv4 { + primary = "192.168.0.2" + secondary = "192.168.0.3" + network_mask = "255.255.255.224" + default_gateway = "192.168.0.1" + } +} +``` + + +## Schema + +### Required + +- `name` (String) Name of Precision Time Service. Applicable values: Maximum: 24 characters; Allowed characters: alpha-numeric, hyphens ('-') and underscores ('_') +- `type` (String) Choose type of Precision Time Service + +### Optional + +- `advance_configuration` (List of Object) An object that has advanced configuration options. (see [below for nested schema](#nestedatt--advance_configuration)) +- `connections` (Block List) An array of objects with unique identifiers of connections. (see [below for nested schema](#nestedblock--connections)) +- `description` (String) Optional description of time service +- `ipv4` (Block, Optional) An object that has Network IP Configurations for Timing Master Servers. (see [below for nested schema](#nestedblock--ipv4)) +- `package` (Block, Optional) Precision Time Service Package Details (see [below for nested schema](#nestedblock--package)) +- `project_id` (String) Equinix Fabric Project ID +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `account` (List of Object) Equinix User Account associated with Precision Time Service (see [below for nested schema](#nestedatt--account)) +- `href` (String) Equinix generated Portal link for the created Precision Time Service +- `id` (String) The unique identifier of the resource +- `state` (String) Indicator of the state of this Precision Time Service. One of: [[PROVISIONED PROVISIONING PROVISIONING_FAILED CONFIGURING CANCELLED DEPROVISIONING_FAILED PENDING_CONFIGURATION DEPROVISIONED CONFIGURING_FAILED DEPROVISIONING]] +- `uuid` (String) Equinix generated id for the Precision Time Service + + +### Nested Schema for `advance_configuration` + +Optional: + +- `ntp` (List of Object) (see [below for nested schema](#nestedobjatt--advance_configuration--ntp)) +- `ptp` (List of Object) (see [below for nested schema](#nestedobjatt--advance_configuration--ptp)) + + +### Nested Schema for `advance_configuration.ntp` + +Optional: + +- `id` (String) +- `password` (String) +- `type` (String) + + + +### Nested Schema for `advance_configuration.ptp` + +Optional: + +- `domain` (Number) +- `grant_time` (Number) +- `log_announce_interval` (Number) +- `log_delay_req_interval` (Number) +- `log_sync_interval` (Number) +- `priority_1` (Number) +- `priority_2` (Number) +- `time_scale` (String) +- `transport_mode` (String) + + + + +### Nested Schema for `connections` + +Required: + +- `uuid` (String) Equinix Fabric Connection UUID; Precision Time Service will be connected with it + +Read-Only: + +- `href` (String) Link to the Equinix Fabric Connection associated with the Precision Time Service +- `type` (String) Type of the Equinix Fabric Connection associated with the Precision Time Service + + + +### Nested Schema for `ipv4` + +Required: + +- `default_gateway` (String) IPv4 address that establishes the Routing Interface where traffic is directed. It serves as the next hop in the Network. +- `network_mask` (String) IPv4 address that defines the range of consecutive subnets in the network. +- `primary` (String) IPv4 address for the Primary Timing Master Server. +- `secondary` (String) IPv4 address for the Secondary Timing Master Server. + + + +### Nested Schema for `package` + +Required: + +- `code` (String) Time Precision Package Code for the desired billing package + +Optional: + +- `href` (String) Time Precision Package HREF link to corresponding resource in Equinix Portal + +Read-Only: + +- `accuracy_avg_max` (Number) Average maximum accuracy provided by the Precision Time Service +- `accuracy_avg_min` (Number) Average minimum accuracy provided by the Precision Time Service +- `accuracy_sla` (Number) SLA for the accuracy provided by the Precision Time Service +- `accuracy_unit` (String) Time unit of accuracy for the Precision Time Service; e.g. microseconds +- `bandwidth` (Number) Bandwidth of the Precision Time Service +- `clients_per_second_max` (Number) Maximum clients available per second for the Precision Time Service +- `multi_subnet_supported` (Boolean) Boolean flag indicating if this Precision Time Service supports multi subnetting +- `redundancy_supported` (Boolean) Boolean flag indicating if this Precision Time Service supports redundancy +- `type` (String) Type of the Precision Time Service Package + + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +- `delete` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Setting a timeout for a Delete operation is only applicable if changes are saved into state before the destroy operation occurs. +- `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. +- `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). + + + +### Nested Schema for `account` + +Read-Only: + +- `account_number` (Number) +- `global_org_id` (String) +- `is_reseller_account` (Boolean) +- `org_id` (String) + + + diff --git a/examples/data-sources/fabric_precision_time/get_by_uuid.tf b/examples/data-sources/fabric_precision_time/get_by_uuid.tf new file mode 100644 index 000000000..dc32aa58a --- /dev/null +++ b/examples/data-sources/fabric_precision_time/get_by_uuid.tf @@ -0,0 +1,3 @@ +data "equinix_fabric_precision_time" "time_service" { + uuid = "f9361b9a-a0b3-4fa3-bf36-be265dc7617f" +} diff --git a/examples/resources/fabric_precision_time/ntp_time_service.tf b/examples/resources/fabric_precision_time/ntp_time_service.tf new file mode 100644 index 000000000..4fa6723be --- /dev/null +++ b/examples/resources/fabric_precision_time/ntp_time_service.tf @@ -0,0 +1,17 @@ +resource "equinix_fabric_precision_time" "ntp" { + type = "NTP" + name = "tf_ntp_PFCR" + description = "Equinix Precision Time with NTP Configuration" + package { + code = "NTP_STANDARD" + } + connections { + uuid = "30b82c65-ffb4-47d3-ab2b-3cacf46d5b8b" + } + ipv4 { + primary = "192.168.0.2" + secondary = "192.168.0.3" + network_mask = "255.255.255.224" + default_gateway = "192.168.0.1" + } +} diff --git a/examples/resources/fabric_precision_time/ptp_time_service.tf b/examples/resources/fabric_precision_time/ptp_time_service.tf new file mode 100644 index 000000000..0870ad644 --- /dev/null +++ b/examples/resources/fabric_precision_time/ptp_time_service.tf @@ -0,0 +1,17 @@ +resource "equinix_fabric_precision_time" "ptp" { + type = "PTP" + name = "tf_ept_PFCR" + description = "Equinix Precision Time with PTP Configuration" + package { + code = "PTP_STANDARD" + } + connections { + uuid = "30b82c65-ffb4-47d3-ab2b-3cacf46d5b8b" + } + ipv4 { + primary = "192.168.0.2" + secondary = "192.168.0.3" + network_mask = "255.255.255.224" + default_gateway = "192.168.0.1" + } +} diff --git a/internal/config/config.go b/internal/config/config.go index 08513984f..d00037a19 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -76,6 +76,8 @@ type Config struct { TerraformVersion string } +type ctxKey string + // Load function validates configuration structure fields and configures // all required API clients. func (c *Config) Load(ctx context.Context) error { @@ -122,8 +124,9 @@ func (c *Config) Load(ctx context.Context) error { // NewFabricClientForSDK returns a terraform sdkv2 plugin compatible // equinix-sdk-go/fabricv4 client to be used to access Fabric's V4 APIs +// Deprecated: migrate to NewFabricClientForFramework instead func (c *Config) NewFabricClientForSDK(d *schema.ResourceData) *fabricv4.APIClient { - client := c.newFabricClient() + client := c.newFabricClient(context.WithValue(context.Background(), ctxKey("fabricSDKv2"), "no_oauth_reload")) baseUserAgent := c.tfSdkUserAgent(client.GetConfig().UserAgent) client.GetConfig().UserAgent = generateModuleUserAgentString(d, baseUserAgent) @@ -134,18 +137,37 @@ func (c *Config) NewFabricClientForSDK(d *schema.ResourceData) *fabricv4.APIClie // Shim for Fabric tests. // Deprecated: when the acceptance package starts to contain API clients for testing/cleanup this will move with them func (c *Config) NewFabricClientForTesting() *fabricv4.APIClient { - client := c.newFabricClient() + client := c.newFabricClient(context.WithValue(context.Background(), ctxKey("fabricSDKv2"), "no_oauth_reload")) client.GetConfig().UserAgent = fmt.Sprintf("tf-acceptance-tests %v", client.GetConfig().UserAgent) return client } +func (c *Config) NewFabricClientForFramework(ctx context.Context, meta tfsdk.Config) *fabricv4.APIClient { + client := c.newFabricClient(ctx) + + baseUserAgent := c.tfFrameworkUserAgent(client.GetConfig().UserAgent) + client.GetConfig().UserAgent = generateFwModuleUserAgentString(ctx, meta, baseUserAgent) + + return client +} + // newFabricClient returns the base fabricv4 client that is then used for either the sdkv2 or framework // implementations of the Terraform Provider with exported Methods -func (c *Config) newFabricClient() *fabricv4.APIClient { +func (c *Config) newFabricClient(ctx context.Context) *fabricv4.APIClient { //nolint:staticcheck // We should move to subsystem loggers, but that is a much bigger change transport := logging.NewTransport("Equinix Fabric (fabricv4)", c.authClient.Transport) + if v := ctx.Value(ctxKey("fabricSDKv2")); v == nil { + authConfig := oauth2.Config{ + ClientID: c.ClientID, + ClientSecret: c.ClientSecret, + BaseURL: c.BaseURL, + } + authClient := authConfig.New(ctx) + //nolint:staticcheck // We should move to subsystem loggers, but that is a much bigger change + transport = logging.NewTransport("Equinix Fabric (fabricv4)", authClient.Transport) + } retryClient := retryablehttp.NewClient() retryClient.HTTPClient.Transport = transport diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4e0d18653..5c7ff3c06 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/equinix/terraform-provider-equinix/internal/config" + fabricprecisiontime "github.com/equinix/terraform-provider-equinix/internal/resources/fabric/precision_time" metalconnection "github.com/equinix/terraform-provider-equinix/internal/resources/metal/connection" metalgateway "github.com/equinix/terraform-provider-equinix/internal/resources/metal/gateway" metalorganization "github.com/equinix/terraform-provider-equinix/internal/resources/metal/organization" @@ -114,6 +115,7 @@ func (p *FrameworkProvider) MetaSchema( func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ + fabricprecisiontime.NewResource, metalgateway.NewResource, metalproject.NewResource, metalprojectsshkey.NewResource, @@ -127,6 +129,7 @@ func (p *FrameworkProvider) Resources(ctx context.Context) []func() resource.Res func (p *FrameworkProvider) DataSources(ctx context.Context) []func() datasource.DataSource { return []func() datasource.DataSource{ + fabricprecisiontime.NewDataSource, metalgateway.NewDataSource, metalproject.NewDataSource, metalprojectsshkey.NewDataSource, diff --git a/internal/resources/fabric/precision_time/datasources.go b/internal/resources/fabric/precision_time/datasources.go new file mode 100644 index 000000000..8d55e7e58 --- /dev/null +++ b/internal/resources/fabric/precision_time/datasources.go @@ -0,0 +1,82 @@ +package precision_time + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + "github.com/equinix/terraform-provider-equinix/internal/framework" +) + +func NewDataSource() datasource.DataSource { + return &DataSource{ + BaseDataSource: framework.NewBaseDataSource( + framework.BaseDataSourceConfig{ + Name: "equinix_fabric_precision_time", + }, + ), + } +} + +type DataSource struct { + framework.BaseDataSource +} + +func (r *DataSource) Schema( + ctx context.Context, + req datasource.SchemaRequest, + resp *datasource.SchemaResponse, +) { + resp.Schema = dataSourceSchema(ctx) +} + +func (r *DataSource) Read( + ctx context.Context, + req datasource.ReadRequest, + resp *datasource.ReadResponse, +) { + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + // Retrieve values from plan + var data PrecisionTimeModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + // Extract the ID of the resource from the config + id := data.Uuid.ValueString() + + // Use API client to get the current state of the resource + ept, _, err := client.PrecisionTimeApi.GetTimeServicesById(ctx, id). + Execute() + + if err != nil { + // If the Precision time Service is not found, remove it from the state + if equinix_errors.IsNotFound(err) { + resp.Diagnostics.AddWarning( + "Precision Time Service", + fmt.Sprintf("[WARN] Precision Time Service (%s) not found, removing from state", id), + ) + resp.State.RemoveResource(ctx) + return + } + + resp.Diagnostics.AddError( + "Error reading Precision Time Service", + "Could not read Precision Time Service with ID "+id+": "+err.Error(), + ) + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(data.parse(ctx, ept)...) + if resp.Diagnostics.HasError() { + return + } + + // Update the Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/internal/resources/fabric/precision_time/datasources_schema.go b/internal/resources/fabric/precision_time/datasources_schema.go new file mode 100644 index 000000000..877bad125 --- /dev/null +++ b/internal/resources/fabric/precision_time/datasources_schema.go @@ -0,0 +1,173 @@ +package precision_time + +import ( + "context" + "fmt" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/equinix/terraform-provider-equinix/internal/framework" + fwtypes "github.com/equinix/terraform-provider-equinix/internal/framework/types" +) + +func dataSourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttributeDefaultDescription(), + "uuid": schema.StringAttribute{ + Description: "Uuid of Precision Time Service resource; used for lookup", + Required: true, + }, + "type": schema.StringAttribute{ + Description: "Choose type of Precision Time Service", + Computed: true, + Validators: []validator.String{ + stringvalidator.OneOf( + string(fabricv4.PRECISIONTIMESERVICEREQUESTTYPE_NTP), + string(fabricv4.PRECISIONTIMESERVICEREQUESTTYPE_PTP), + ), + }, + }, + "href": schema.StringAttribute{ + Description: "Equinix generated Portal link for the created Precision Time Service", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Name of Precision Time Service. Applicable values: Maximum: 24 characters; Allowed characters: alpha-numeric, hyphens ('-') and underscores ('_')", + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(24), + }, + }, + "description": schema.StringAttribute{ + Description: "Optional description of time service", + Computed: true, + }, + "state": schema.StringAttribute{ + Description: fmt.Sprintf("Indicator of the state of this Precision Time Service. One of: [%v]", fabricv4.AllowedPrecisionTimeServiceCreateResponseStateEnumValues), + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "Equinix Fabric Project ID", + Computed: true, + }, + "advance_configuration": schema.ListAttribute{ + Description: "An object that has advanced configuration options.", + CustomType: fwtypes.NewListNestedObjectTypeOf[AdvanceConfigurationModel](ctx), + ElementType: fwtypes.NewObjectTypeOf[AdvanceConfigurationModel](ctx), + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + "account": schema.ListAttribute{ + Description: "Equinix User Account associated with Precision Time Service", + CustomType: fwtypes.NewListNestedObjectTypeOf[AccountModel](ctx), + ElementType: fwtypes.NewObjectTypeOf[AccountModel](ctx), + Computed: true, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + }, + Blocks: map[string]schema.Block{ + "package": schema.SingleNestedBlock{ + Description: "Precision Time Service Package Details", + CustomType: fwtypes.NewObjectTypeOf[PackageModel](ctx), + Attributes: map[string]schema.Attribute{ + "code": schema.StringAttribute{ + Description: "Time Precision Package Code for the desired billing package", + Computed: true, + }, + "href": schema.StringAttribute{ + Description: "Time Precision Package HREF link to corresponding resource in Equinix Portal", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Type of the Precision Time Service Package", + Computed: true, + }, + "bandwidth": schema.Int64Attribute{ + Description: "Bandwidth of the Precision Time Service", + Computed: true, + }, + "clients_per_second_max": schema.Int64Attribute{ + Description: "Maximum clients available per second for the Precision Time Service", + Computed: true, + }, + "redundancy_supported": schema.BoolAttribute{ + Description: "Boolean flag indicating if this Precision Time Service supports redundancy", + Computed: true, + }, + "multi_subnet_supported": schema.BoolAttribute{ + Description: "Boolean flag indicating if this Precision Time Service supports multi subnetting", + Computed: true, + }, + "accuracy_unit": schema.StringAttribute{ + Description: "Time unit of accuracy for the Precision Time Service; e.g. microseconds", + Computed: true, + }, + "accuracy_sla": schema.Int64Attribute{ + Description: "SLA for the accuracy provided by the Precision Time Service", + Computed: true, + }, + "accuracy_avg_min": schema.Int64Attribute{ + Description: "Average minimum accuracy provided by the Precision Time Service", + Computed: true, + }, + "accuracy_avg_max": schema.Int64Attribute{ + Description: "Average maximum accuracy provided by the Precision Time Service", + Computed: true, + }, + }, + }, + "ipv4": schema.SingleNestedBlock{ + Description: "An object that has Network IP Configurations for Timing Master Servers.", + CustomType: fwtypes.NewObjectTypeOf[Ipv4Model](ctx), + Attributes: map[string]schema.Attribute{ + "primary": schema.StringAttribute{ + Description: "IPv4 address for the Primary Timing Master Server.", + Computed: true, + }, + "secondary": schema.StringAttribute{ + Description: "IPv4 address for the Secondary Timing Master Server.", + Computed: true, + }, + "network_mask": schema.StringAttribute{ + Description: "IPv4 address that defines the range of consecutive subnets in the network.", + Computed: true, + }, + "default_gateway": schema.StringAttribute{ + Description: "IPv4 address that establishes the Routing Interface where traffic is directed. It serves as the next hop in the Network.", + Computed: true, + }, + }, + }, + "connections": schema.ListNestedBlock{ + Description: "An array of objects with unique identifiers of connections.", + CustomType: fwtypes.NewListNestedObjectTypeOf[ConnectionModel](ctx), + NestedObject: schema.NestedBlockObject{ + CustomType: fwtypes.NewObjectTypeOf[ConnectionModel](ctx), + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Description: "Equinix Fabric Connection UUID; Precision Time Service will be connected with it", + Computed: true, + }, + "href": schema.StringAttribute{ + Description: "Link to the Equinix Fabric Connection associated with the Precision Time Service", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Type of the Equinix Fabric Connection associated with the Precision Time Service", + Computed: true, + }, + }, + }, + }, + }, + } +} diff --git a/internal/resources/fabric/precision_time/datasources_test.go b/internal/resources/fabric/precision_time/datasources_test.go new file mode 100644 index 000000000..5559a2080 --- /dev/null +++ b/internal/resources/fabric/precision_time/datasources_test.go @@ -0,0 +1,4 @@ +package precision_time + +// Tested in resource test because of the expense to create connection and precision time +// resources before performing data retrieval diff --git a/internal/resources/fabric/precision_time/models.go b/internal/resources/fabric/precision_time/models.go new file mode 100644 index 000000000..330c667bb --- /dev/null +++ b/internal/resources/fabric/precision_time/models.go @@ -0,0 +1,332 @@ +package precision_time + +import ( + "context" + "reflect" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + + fwtypes "github.com/equinix/terraform-provider-equinix/internal/framework/types" +) + +type PrecisionTimeModel struct { + ID types.String `tfsdk:"id"` + Type types.String `tfsdk:"type"` + Href types.String `tfsdk:"href"` + Uuid types.String `tfsdk:"uuid"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + State types.String `tfsdk:"state"` + Package fwtypes.ObjectValueOf[PackageModel] `tfsdk:"package"` + Connections fwtypes.ListNestedObjectValueOf[ConnectionModel] `tfsdk:"connections"` + Ipv4 fwtypes.ObjectValueOf[Ipv4Model] `tfsdk:"ipv4"` + Account fwtypes.ListNestedObjectValueOf[AccountModel] `tfsdk:"account"` + AdvanceConfiguration fwtypes.ListNestedObjectValueOf[AdvanceConfigurationModel] `tfsdk:"advance_configuration"` + ProjectId types.String `tfsdk:"project_id"` + Timeouts timeouts.Value `tfsdk:"timeouts"` +} + +type PackageModel struct { + Code types.String `tfsdk:"code"` + Href types.String `tfsdk:"href"` + Type types.String `tfsdk:"type"` + Bandwidth types.Int64 `tfsdk:"bandwidth"` + ClientsPerSecondMax types.Int64 `tfsdk:"clients_per_second_max"` + RedundancySupported types.Bool `tfsdk:"redundancy_supported"` + MultiSubnetSupported types.Bool `tfsdk:"multi_subnet_supported"` + AccuracyUnit types.String `tfsdk:"accuracy_unit"` + AccuracySla types.Int64 `tfsdk:"accuracy_sla"` + AccuracyAvgMin types.Int64 `tfsdk:"accuracy_avg_min"` + AccuracyAvgMax types.Int64 `tfsdk:"accuracy_avg_max"` +} + +type ConnectionModel struct { + Uuid types.String `tfsdk:"uuid"` + Href types.String `tfsdk:"href"` + Type types.String `tfsdk:"type"` +} + +type Ipv4Model struct { + Primary types.String `tfsdk:"primary"` + Secondary types.String `tfsdk:"secondary"` + NetworkMask types.String `tfsdk:"network_mask"` + DefaultGateway types.String `tfsdk:"default_gateway"` +} + +type AccountModel struct { + AccountNumber types.Int64 `tfsdk:"account_number"` + IsResellerAccount types.Bool `tfsdk:"is_reseller_account"` + OrgId types.String `tfsdk:"org_id"` + GlobalOrgId types.String `tfsdk:"global_org_id"` +} + +type AdvanceConfigurationModel struct { + Ntp fwtypes.ListNestedObjectValueOf[MD5Model] `tfsdk:"ntp"` + Ptp fwtypes.ListNestedObjectValueOf[PTPModel] `tfsdk:"ptp"` +} + +type MD5Model struct { + Type types.String `tfsdk:"type"` + Id types.String `tfsdk:"id"` + Password types.String `tfsdk:"password"` +} + +type PTPModel struct { + TimeScale types.String `tfsdk:"time_scale"` + Domain types.Int64 `tfsdk:"domain"` + Priority1 types.Int64 `tfsdk:"priority_1"` + Priority2 types.Int64 `tfsdk:"priority_2"` + LogAnnounceInterval types.Int64 `tfsdk:"log_announce_interval"` + LogSyncInterval types.Int64 `tfsdk:"log_sync_interval"` + LogDelayReqInterval types.Int64 `tfsdk:"log_delay_req_interval"` + TransportMode types.String `tfsdk:"transport_mode"` + GrantTime types.Int64 `tfsdk:"grant_time"` +} + +func (m *PrecisionTimeModel) parse(ctx context.Context, ept *fabricv4.PrecisionTimeServiceCreateResponse) diag.Diagnostics { + diags := parsePrecisionTime(ctx, ept, + &m.ID, &m.Type, &m.Href, &m.Uuid, &m.Name, &m.Description, + &m.State, &m.ProjectId, &m.Package, &m.Ipv4, + &m.Account, + &m.AdvanceConfiguration, + &m.Connections, + ) + + return diags +} + +func parsePrecisionTime( + ctx context.Context, + ept *fabricv4.PrecisionTimeServiceCreateResponse, + id, type_, href, uuid, name, description, state, projectId *basetypes.StringValue, + package_ *fwtypes.ObjectValueOf[PackageModel], + ipv4 *fwtypes.ObjectValueOf[Ipv4Model], + account *fwtypes.ListNestedObjectValueOf[AccountModel], + advanceConfiguration *fwtypes.ListNestedObjectValueOf[AdvanceConfigurationModel], + connections *fwtypes.ListNestedObjectValueOf[ConnectionModel], +) diag.Diagnostics { + var diags diag.Diagnostics + + *id = types.StringValue(ept.GetUuid()) + *type_ = types.StringValue(string(ept.GetType())) + *uuid = types.StringValue(ept.GetUuid()) + *state = types.StringValue(string(ept.GetState())) + *href = types.StringValue(ept.GetHref()) + + if ept.GetName() != "" { + *name = types.StringValue(ept.GetName()) + } + if ept.GetDescription() != "" { + *description = types.StringValue(ept.GetDescription()) + } + + eptProject := ept.GetProject() + if eptProject.GetProjectId() != "" { + *projectId = types.StringValue(eptProject.GetProjectId()) + } + + eptPackage := ept.GetPackage() + parsedEptPackage, diags := parsePackage(ctx, &eptPackage) + if diags.HasError() { + return diags + } + if !reflect.DeepEqual(parsedEptPackage, fwtypes.NewObjectValueOfNull[PackageModel](ctx)) { + *package_ = parsedEptPackage + } + + parsedEptConnections, diags := parseConnections(ctx, ept.GetConnections()) + if diags.HasError() { + return diags + } + if !reflect.DeepEqual(parsedEptConnections, fwtypes.NewListNestedObjectValueOfValueSlice[ConnectionModel](ctx, []ConnectionModel{})) { + *connections = parsedEptConnections + } + + eptIpv4 := ept.GetIpv4() + parsedEptIpv4, diags := parseIpv4(ctx, &eptIpv4) + if diags.HasError() { + return diags + } + if !reflect.DeepEqual(parsedEptIpv4, fwtypes.NewObjectValueOfNull[Ipv4Model](ctx)) { + *ipv4 = parsedEptIpv4 + } + + eptAccount := ept.GetAccount() + parsedEptAccount, diags := parseAccount(ctx, &eptAccount) + if diags.HasError() { + return diags + } + if !reflect.DeepEqual(parsedEptAccount, fwtypes.NewObjectValueOfNull[AccountModel](ctx)) { + *account = parsedEptAccount + } + + eptAdvanceConfiguration := ept.GetAdvanceConfiguration() + parsedEptAdvanceConfiguration, diags := parseAdvanceConfiguration(ctx, &eptAdvanceConfiguration) + if diags.HasError() { + return diags + } + if !reflect.DeepEqual(parsedEptAdvanceConfiguration, fwtypes.NewListNestedObjectValueOfNull[AdvanceConfigurationModel](ctx)) { + *advanceConfiguration = parsedEptAdvanceConfiguration + } + + return diags +} + +func parsePackage(ctx context.Context, package_ *fabricv4.PrecisionTimePackageResponse) (fwtypes.ObjectValueOf[PackageModel], diag.Diagnostics) { + packageModel := &PackageModel{} + + packageModel.Code = types.StringValue(string(package_.GetCode())) + if package_.GetHref() != "" { + packageModel.Href = types.StringValue(package_.GetHref()) + } + if string(package_.GetType()) != "" { + packageModel.Type = types.StringValue(string(package_.GetType())) + } + if package_.GetAccuracyUnit() != "" { + packageModel.AccuracyUnit = types.StringValue(package_.GetAccuracyUnit()) + } + packageModel.AccuracySla = types.Int64Value(int64(package_.GetAccuracySla())) + packageModel.AccuracyAvgMin = types.Int64Value(int64(package_.GetAccuracyAvgMin())) + packageModel.AccuracyAvgMax = types.Int64Value(int64(package_.GetAccuracyAvgMax())) + packageModel.Bandwidth = types.Int64Value(int64(package_.GetBandwidth())) + packageModel.ClientsPerSecondMax = types.Int64Value(int64(package_.GetClientsPerSecondMax())) + packageModel.RedundancySupported = types.BoolValue(package_.GetRedundancySupported()) + packageModel.MultiSubnetSupported = types.BoolValue(package_.GetMultiSubnetSupported()) + + return fwtypes.NewObjectValueOf[PackageModel](ctx, packageModel), nil +} + +func parseConnections(ctx context.Context, connections []fabricv4.FabricConnectionUuid) (fwtypes.ListNestedObjectValueOf[ConnectionModel], diag.Diagnostics) { + connectionModels := make([]ConnectionModel, len(connections)) + + for index, connection := range connections { + connectionModel := ConnectionModel{ + Uuid: types.StringValue(connection.GetUuid()), + } + if connection.GetHref() != "" { + connectionModel.Href = types.StringValue(connection.GetHref()) + } + if connection.GetType() != "" { + connectionModel.Type = types.StringValue(connection.GetType()) + } + connectionModels[index] = connectionModel + } + + return fwtypes.NewListNestedObjectValueOfValueSlice(ctx, connectionModels), nil +} + +func parseIpv4(ctx context.Context, ipv4 *fabricv4.Ipv4) (fwtypes.ObjectValueOf[Ipv4Model], diag.Diagnostics) { + ipv4Model := &Ipv4Model{} + + ipv4Model.Primary = types.StringValue(ipv4.GetPrimary()) + ipv4Model.Secondary = types.StringValue(ipv4.GetSecondary()) + ipv4Model.DefaultGateway = types.StringValue(ipv4.GetDefaultGateway()) + ipv4Model.NetworkMask = types.StringValue(ipv4.GetNetworkMask()) + + return fwtypes.NewObjectValueOf[Ipv4Model](ctx, ipv4Model), nil +} + +func parseAccount(ctx context.Context, account *fabricv4.Account) (fwtypes.ListNestedObjectValueOf[AccountModel], diag.Diagnostics) { + accountModel := AccountModel{} + + if account.GetAccountNumber() != 0 { + accountModel.AccountNumber = types.Int64Value(int64(account.GetAccountNumber())) + } + if account.IsResellerAccount != nil { + accountModel.IsResellerAccount = types.BoolValue(account.GetIsResellerAccount()) + } + if account.OrgId != nil { + accountModel.OrgId = types.StringValue(account.GetOrgId()) + } + if account.GlobalOrgId != nil { + accountModel.GlobalOrgId = types.StringValue(account.GetGlobalOrgId()) + } + + return fwtypes.NewListNestedObjectValueOfValueSlice[AccountModel](ctx, []AccountModel{accountModel}), nil +} + +func parseAdvanceConfiguration(ctx context.Context, advConfig *fabricv4.AdvanceConfiguration) (fwtypes.ListNestedObjectValueOf[AdvanceConfigurationModel], diag.Diagnostics) { + var diags diag.Diagnostics + advConfigModel := AdvanceConfigurationModel{} + + md5s, diags := parseNtp(ctx, advConfig.GetNtp()) + if diags.HasError() { + return fwtypes.NewListNestedObjectValueOfUnknown[AdvanceConfigurationModel](ctx), diags + } + if len(md5s.Elements()) > 0 { + advConfigModel.Ntp = md5s + } else { + advConfigModel.Ntp = fwtypes.NewListNestedObjectValueOfNull[MD5Model](ctx) + } + + ptp := advConfig.GetPtp() + parsedPtp, diags := parsePtp(ctx, &ptp) + if diags.HasError() { + return fwtypes.NewListNestedObjectValueOfUnknown[AdvanceConfigurationModel](ctx), diags + } + if len(parsedPtp.Elements()) > 0 { + advConfigModel.Ptp = parsedPtp + } else { + advConfigModel.Ptp = fwtypes.NewListNestedObjectValueOfNull[PTPModel](ctx) + } + + if reflect.DeepEqual(advConfigModel, AdvanceConfigurationModel{}) { + return fwtypes.NewListNestedObjectValueOfNull[AdvanceConfigurationModel](ctx), nil + } + + return fwtypes.NewListNestedObjectValueOfValueSlice[AdvanceConfigurationModel](ctx, []AdvanceConfigurationModel{advConfigModel}), nil +} + +func parseNtp(ctx context.Context, ntp []fabricv4.Md5) (fwtypes.ListNestedObjectValueOf[MD5Model], diag.Diagnostics) { + ntpModel := make([]MD5Model, len(ntp)) + + for index, md5 := range ntp { + ntpModel[index] = MD5Model{ + Type: types.StringValue(string(md5.GetType())), + Id: types.StringValue(md5.GetId()), + Password: types.StringValue(md5.GetPassword()), + } + } + + return fwtypes.NewListNestedObjectValueOfValueSlice(ctx, ntpModel), nil +} + +func parsePtp(ctx context.Context, ptp *fabricv4.PtpAdvanceConfiguration) (fwtypes.ListNestedObjectValueOf[PTPModel], diag.Diagnostics) { + ptpModel := PTPModel{} + + if timeScale, ok := ptp.GetTimeScaleOk(); ok && timeScale != nil { + ptpModel.TimeScale = types.StringValue(string(ptp.GetTimeScale())) + } + if domain, ok := ptp.GetDomainOk(); ok && domain != nil { + ptpModel.Domain = types.Int64Value(int64(ptp.GetDomain())) + } + if priority1, ok := ptp.GetPriority1Ok(); ok && priority1 != nil { + ptpModel.Priority1 = types.Int64Value(int64(ptp.GetPriority1())) + } + if priority2, ok := ptp.GetPriority2Ok(); ok && priority2 != nil { + ptpModel.Priority2 = types.Int64Value(int64(ptp.GetPriority2())) + } + if logAnnounceInterval, ok := ptp.GetLogAnnounceIntervalOk(); ok && logAnnounceInterval != nil { + ptpModel.LogAnnounceInterval = types.Int64Value(int64(ptp.GetLogAnnounceInterval())) + } + if logSyncInterval, ok := ptp.GetLogSyncIntervalOk(); ok && logSyncInterval != nil { + ptpModel.LogSyncInterval = types.Int64Value(int64(ptp.GetLogSyncInterval())) + } + if logDelayReqInterval, ok := ptp.GetLogDelayReqIntervalOk(); ok && logDelayReqInterval != nil { + ptpModel.LogDelayReqInterval = types.Int64Value(int64(ptp.GetLogDelayReqInterval())) + } + if transportMode, ok := ptp.GetTransportModeOk(); ok && transportMode != nil { + ptpModel.TransportMode = types.StringValue(string(ptp.GetTransportMode())) + + } + if grantTime, ok := ptp.GetGrantTimeOk(); ok && grantTime != nil { + ptpModel.GrantTime = types.Int64Value(int64(ptp.GetGrantTime())) + } + + return fwtypes.NewListNestedObjectValueOfValueSlice[PTPModel](ctx, []PTPModel{ptpModel}), nil + +} diff --git a/internal/resources/fabric/precision_time/resource.go b/internal/resources/fabric/precision_time/resource.go new file mode 100644 index 000000000..406902bea --- /dev/null +++ b/internal/resources/fabric/precision_time/resource.go @@ -0,0 +1,463 @@ +package precision_time + +import ( + "context" + "fmt" + "net/http" + "reflect" + "slices" + "time" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + + equinix_errors "github.com/equinix/terraform-provider-equinix/internal/errors" + "github.com/equinix/terraform-provider-equinix/internal/framework" +) + +func NewResource() resource.Resource { + return &Resource{ + BaseResource: framework.NewBaseResource( + framework.BaseResourceConfig{ + Name: "equinix_fabric_precision_time", + }, + ), + } +} + +type Resource struct { + framework.BaseResource +} + +func (r *Resource) Schema( + ctx context.Context, + req resource.SchemaRequest, + resp *resource.SchemaResponse, +) { + resp.Schema = resourceSchema(ctx) +} + +func (r *Resource) Create( + ctx context.Context, + req resource.CreateRequest, + resp *resource.CreateResponse, +) { + + var plan PrecisionTimeModel + diags := req.Plan.Get(ctx, &plan) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve the API client from the provider metadata + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + createRequest, diags := buildCreateRequest(ctx, plan) + + resp.Diagnostics.Append(diags...) + if diags.HasError() { + return + } + + ept, _, err := client.PrecisionTimeApi.CreateTimeServices(ctx).PrecisionTimeServiceRequest(createRequest).Execute() + if err != nil { + resp.Diagnostics.AddError( + "Error creating Precision Time Service", + equinix_errors.FormatFabricError(err).Error(), + ) + return + } + + createTimeout, diags := plan.Timeouts.Create(ctx, 10*time.Minute) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + createWaiter := getCreateUpdateWaiter(ctx, client, ept.GetUuid(), createTimeout) + eptChecked, err := createWaiter.WaitForStateContext(ctx) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to create Precision Time Service %s", ept.GetUuid()), err.Error()) + return + } + + // Parse API response into the Terraform state + resp.Diagnostics.Append(plan.parse(ctx, eptChecked.(*fabricv4.PrecisionTimeServiceCreateResponse))...) + if resp.Diagnostics.HasError() { + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *Resource) Read( + ctx context.Context, + req resource.ReadRequest, + resp *resource.ReadResponse, +) { + var state PrecisionTimeModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Retrieve the API client from the provider metadata + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + // Extract the ID of the resource from the state + id := state.ID.ValueString() + + ept, _, err := client.PrecisionTimeApi.GetTimeServicesById(ctx, id).Execute() + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed retrieving Precision Time Service %s", id), err.Error()) + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(state.parse(ctx, ept)...) + if resp.Diagnostics.HasError() { + return + } + + // Update the Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &state)...) +} + +func (r *Resource) Update( + ctx context.Context, + req resource.UpdateRequest, + resp *resource.UpdateResponse, +) { + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + // Retrieve values from plan + var state, plan PrecisionTimeModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + if resp.Diagnostics.HasError() { + return + } + + // Extract the ID of the resource from the state + id := plan.ID.ValueString() + + // Prepare update request based on the changes + updateRequest := make([]fabricv4.PrecisionTimeChangeOperation, 0) + + if !state.Name.Equal(plan.Name) { + op := fabricv4.PRECISIONTIMECHANGEOPERATIONOP_REPLACE + if plan.Name.ValueString() != "" && state.Name.ValueString() == "" { + op = fabricv4.PRECISIONTIMECHANGEOPERATIONOP_ADD + } else if plan.Name.ValueString() == "" && state.Name.ValueString() != "" { + op = fabricv4.PRECISIONTIMECHANGEOPERATIONOP_REMOVE + } + updateRequest = append(updateRequest, fabricv4.PrecisionTimeChangeOperation{ + Op: op, + Path: fabricv4.PRECISIONTIMECHANGEOPERATIONPATH_NAME, + Value: plan.Name.ValueString(), + }) + } + if !state.Package.Equal(plan.Package) { + packageModel := PackageModel{} + diags := plan.Package.As(ctx, &packageModel, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + }) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + updateRequest = append(updateRequest, fabricv4.PrecisionTimeChangeOperation{ + Op: fabricv4.PRECISIONTIMECHANGEOPERATIONOP_REPLACE, + Path: fabricv4.PRECISIONTIMECHANGEOPERATIONPATH_PACKAGE, + Value: packageModel.Code.ValueString(), + }) + } + + if len(updateRequest) > 1 { + resp.Diagnostics.AddError("Error updating Precision Time Service", + "This resource only accepts one attribute change at a time; please reduce changes and try again") + return + } + for _, update := range updateRequest { + if !reflect.DeepEqual(updateRequest, fabricv4.PrecisionTimeChangeOperation{}) { + _, _, err := client.PrecisionTimeApi.UpdateTimeServicesById(ctx, id). + PrecisionTimeChangeOperation([]fabricv4.PrecisionTimeChangeOperation{update}). + Execute() + + if err != nil { + resp.Diagnostics.AddError( + "Error updating Precision Time Service", + equinix_errors.FormatFabricError(err).Error(), + ) + } + } + } + + updateTimeout, diags := plan.Timeouts.Create(ctx, 10*time.Minute) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + updateWaiter := getCreateUpdateWaiter(ctx, client, id, updateTimeout) + ept, err := updateWaiter.WaitForStateContext(ctx) + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to update Precision Time Service %s", id), err.Error()) + return + } + + // Set state to fully populated data + resp.Diagnostics.Append(plan.parse(ctx, ept.(*fabricv4.PrecisionTimeServiceCreateResponse))...) + if resp.Diagnostics.HasError() { + return + } + + // Set the updated state back into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (r *Resource) Delete( + ctx context.Context, + req resource.DeleteRequest, + resp *resource.DeleteResponse, +) { + // Retrieve the API client + client := r.Meta.NewFabricClientForFramework(ctx, req.ProviderMeta) + + // Retrieve the current state + var state PrecisionTimeModel + diags := req.State.Get(ctx, &state) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Extract the ID of the resource from the state + id := state.ID.ValueString() + + // API call to delete the Precision Time Service + _, deleteResp, err := client.PrecisionTimeApi.DeleteTimeServiceById(ctx, id).Execute() + if err != nil { + if deleteResp == nil || !slices.Contains([]int{http.StatusForbidden, http.StatusNotFound}, deleteResp.StatusCode) { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to delete Precision Time Service %s", id), err.Error()) + } + } + + deleteTimeout, diags := state.Timeouts.Create(ctx, 10*time.Minute) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + deleteWaiter := getDeleteWaiter(ctx, client, id, deleteTimeout) + _, err = deleteWaiter.WaitForStateContext(ctx) + + if err != nil { + resp.Diagnostics.AddError( + fmt.Sprintf("Failed to delete Precision Time Service %s", id), err.Error()) + } +} + +func buildCreateRequest(ctx context.Context, plan PrecisionTimeModel) (fabricv4.PrecisionTimeServiceRequest, diag.Diagnostics) { + var diags diag.Diagnostics + request := fabricv4.PrecisionTimeServiceRequest{ + Type: fabricv4.PrecisionTimeServiceRequestType(plan.Type.ValueString()), + Name: plan.Name.ValueString(), + } + + if plan.Description.ValueString() != "" { + request.SetDescription(plan.Description.ValueString()) + } + + packageModel := PackageModel{} + diags = plan.Package.As(ctx, &packageModel, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + }) + if diags.HasError() { + return fabricv4.PrecisionTimeServiceRequest{}, diags + } + + package_ := fabricv4.PrecisionTimePackageRequest{ + Code: fabricv4.GetTimeServicesPackageByCodePackageCodeParameter(packageModel.Code.ValueString()), + } + + if href := packageModel.Href.ValueString(); href != "" { + package_.SetHref(href) + } + request.SetPackage(package_) + + connectionsModels, diags := plan.Connections.ToSlice(ctx) + if diags.HasError() { + return fabricv4.PrecisionTimeServiceRequest{}, diags + } + + connections := make([]fabricv4.FabricConnectionUuid, len(connectionsModels)) + for index, connection := range connectionsModels { + connections[index].SetUuid(connection.Uuid.ValueString()) + + if type_ := connection.Type.ValueString(); type_ != "" { + connections[index].SetType(type_) + } + + if href := connection.Href.ValueString(); href != "" { + connections[index].SetHref(href) + } + } + request.SetConnections(connections) + + ipv4Model := Ipv4Model{} + diags = plan.Ipv4.As(ctx, &ipv4Model, basetypes.ObjectAsOptions{ + UnhandledNullAsEmpty: true, + UnhandledUnknownAsEmpty: true, + }) + if diags.HasError() { + return fabricv4.PrecisionTimeServiceRequest{}, diags + } + + ipv4 := fabricv4.Ipv4{} + ipv4.SetPrimary(ipv4Model.Primary.ValueString()) + ipv4.SetSecondary(ipv4Model.Secondary.ValueString()) + ipv4.SetNetworkMask(ipv4Model.NetworkMask.ValueString()) + ipv4.SetDefaultGateway(ipv4Model.DefaultGateway.ValueString()) + request.SetIpv4(ipv4) + + advConfigList := make([]AdvanceConfigurationModel, 0) + diags = plan.AdvanceConfiguration.ElementsAs(ctx, &advConfigList, true) + if diags.HasError() { + return fabricv4.PrecisionTimeServiceRequest{}, diags + } + + if len(advConfigList) != 0 { + advConfigModel := advConfigList[0] + ptpList := make([]PTPModel, 0) + diags = advConfigModel.Ptp.ElementsAs(ctx, &ptpList, true) + if diags.HasError() { + return fabricv4.PrecisionTimeServiceRequest{}, diags + } + + ptp := fabricv4.PtpAdvanceConfiguration{} + if len(ptpList) != 0 { + ptpModel := ptpList[0] + if timeScale := ptpModel.TimeScale.ValueString(); timeScale != "" { + ptp.SetTimeScale(fabricv4.PtpAdvanceConfigurationTimeScale(timeScale)) + } + if domain := ptpModel.Domain.ValueInt64(); domain != 0 { + ptp.SetPriority1(int32(domain)) + } + if priority1 := ptpModel.Priority1.ValueInt64(); priority1 != 0 { + ptp.SetPriority1(int32(priority1)) + } + if priority2 := ptpModel.Priority2.ValueInt64(); priority2 != 0 { + ptp.SetPriority2(int32(priority2)) + } + if logAnnounceInterval := ptpModel.LogAnnounceInterval.ValueInt64(); logAnnounceInterval != 0 { + ptp.SetLogAnnounceInterval(int32(logAnnounceInterval)) + } + if logSyncInterval := ptpModel.LogSyncInterval.ValueInt64(); logSyncInterval != 0 { + ptp.SetLogSyncInterval(int32(logSyncInterval)) + } + if logDelayReqInterval := ptpModel.LogDelayReqInterval.ValueInt64(); logDelayReqInterval != 0 { + ptp.SetLogDelayReqInterval(int32(logDelayReqInterval)) + } + if transportMode := ptpModel.TransportMode.ValueString(); transportMode != "" { + ptp.SetTransportMode(fabricv4.PtpAdvanceConfigurationTransportMode(transportMode)) + } + if grantTime := ptpModel.GrantTime.ValueInt64(); grantTime != 0 { + ptp.SetGrantTime(int32(grantTime)) + } + } + + ntpModels, diags := advConfigModel.Ntp.ToSlice(ctx) + if diags.HasError() { + return fabricv4.PrecisionTimeServiceRequest{}, diags + } + + ntps := make([]fabricv4.Md5, len(ntpModels)) + for index, md5 := range ntpModels { + ntps[index] = fabricv4.Md5{} + if type_ := md5.Type.ValueString(); type_ != "" { + ntps[index].SetType(fabricv4.Md5Type(type_)) + } + if id := md5.Id.ValueString(); id != "" { + ntps[index].SetId(id) + } + if password := md5.Password.ValueString(); password != "" { + ntps[index].SetPassword(password) + } + } + + advConfig := fabricv4.AdvanceConfiguration{} + if len(ntps) > 0 { + advConfig.SetNtp(ntps) + } + if !reflect.DeepEqual(ptp, fabricv4.PtpAdvanceConfiguration{}) { + advConfig.SetPtp(ptp) + } + if !reflect.DeepEqual(advConfig, fabricv4.AdvanceConfiguration{}) { + request.SetAdvanceConfiguration(advConfig) + } + } + + if plan.ProjectId.ValueString() != "" { + project := fabricv4.Project{} + project.SetProjectId(plan.ProjectId.ValueString()) + request.SetProject(project) + } + + return request, diags +} + +func getCreateUpdateWaiter(ctx context.Context, client *fabricv4.APIClient, id string, timeout time.Duration) *retry.StateChangeConf { + return &retry.StateChangeConf{ + Target: []string{ + string(fabricv4.PRECISIONTIMESERVICECREATERESPONSESTATE_PROVISIONED), + string(fabricv4.PRECISIONTIMESERVICECREATERESPONSESTATE_PENDING_CONFIGURATION), + }, + Refresh: func() (interface{}, string, error) { + ept, _, err := client.PrecisionTimeApi.GetTimeServicesById(ctx, id).Execute() + if err != nil { + return 0, "", err + } + return ept, string(ept.GetState()), nil + }, + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } +} + +func getDeleteWaiter(ctx context.Context, client *fabricv4.APIClient, id string, timeout time.Duration) *retry.StateChangeConf { + // deletedMarker is a terraform-provider-only value that is used by the waiter + // to indicate that the Precision Time Service appears to be deleted successfully based on + // status code + deletedMarker := "tf-marker-for-deleted-precision-time-service" + return &retry.StateChangeConf{ + Target: []string{ + deletedMarker, + string(fabricv4.PRECISIONTIMESERVICECREATERESPONSESTATE_DEPROVISIONED), + }, + Refresh: func() (interface{}, string, error) { + ept, resp, err := client.PrecisionTimeApi.GetTimeServicesById(ctx, id).Execute() + if err != nil { + if resp != nil && slices.Contains([]int{http.StatusForbidden, http.StatusNotFound}, resp.StatusCode) { + return ept, deletedMarker, nil + } + return 0, "", err + } + return ept, string(ept.GetState()), nil + }, + Timeout: timeout, + Delay: 10 * time.Second, + MinTimeout: 5 * time.Second, + } +} diff --git a/internal/resources/fabric/precision_time/resource_schema.go b/internal/resources/fabric/precision_time/resource_schema.go new file mode 100644 index 000000000..9a17cf4ea --- /dev/null +++ b/internal/resources/fabric/precision_time/resource_schema.go @@ -0,0 +1,203 @@ +package precision_time + +import ( + "context" + "fmt" + + "github.com/equinix/equinix-sdk-go/services/fabricv4" + "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + + "github.com/equinix/terraform-provider-equinix/internal/framework" + fwtypes "github.com/equinix/terraform-provider-equinix/internal/framework/types" +) + +func resourceSchema(ctx context.Context) schema.Schema { + return schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttributeDefaultDescription(), + "uuid": schema.StringAttribute{ + Description: "Equinix generated id for the Precision Time Service", + Computed: true, + }, + "href": schema.StringAttribute{ + Description: "Equinix generated Portal link for the created Precision Time Service", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Choose type of Precision Time Service", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + string(fabricv4.PRECISIONTIMESERVICEREQUESTTYPE_NTP), + string(fabricv4.PRECISIONTIMESERVICEREQUESTTYPE_PTP), + ), + }, + }, + "name": schema.StringAttribute{ + Description: "Name of Precision Time Service. Applicable values: Maximum: 24 characters; Allowed characters: alpha-numeric, hyphens ('-') and underscores ('_')", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(24), + }, + }, + "description": schema.StringAttribute{ + Description: "Optional description of time service", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "state": schema.StringAttribute{ + Description: fmt.Sprintf("Indicator of the state of this Precision Time Service. One of: [%v]", fabricv4.AllowedPrecisionTimeServiceCreateResponseStateEnumValues), + Computed: true, + }, + "project_id": schema.StringAttribute{ + Description: "Equinix Fabric Project ID", + Optional: true, + Computed: true, + }, + "advance_configuration": schema.ListAttribute{ + Description: "An object that has advanced configuration options.", + CustomType: fwtypes.NewListNestedObjectTypeOf[AdvanceConfigurationModel](ctx), + ElementType: fwtypes.NewObjectTypeOf[AdvanceConfigurationModel](ctx), + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + "account": schema.ListAttribute{ + Description: "Equinix User Account associated with Precision Time Service", + CustomType: fwtypes.NewListNestedObjectTypeOf[AccountModel](ctx), + ElementType: fwtypes.NewObjectTypeOf[AccountModel](ctx), + Computed: true, + PlanModifiers: []planmodifier.List{ + listplanmodifier.UseStateForUnknown(), + }, + Validators: []validator.List{ + listvalidator.SizeAtMost(1), + }, + }, + }, + Blocks: map[string]schema.Block{ + "timeouts": timeouts.Block(ctx, timeouts.Opts{ + Create: true, + Read: true, + Update: true, + Delete: true, + }), + "package": schema.SingleNestedBlock{ + Description: "Precision Time Service Package Details", + CustomType: fwtypes.NewObjectTypeOf[PackageModel](ctx), + Attributes: map[string]schema.Attribute{ + "code": schema.StringAttribute{ + Description: "Time Precision Package Code for the desired billing package", + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + string(fabricv4.GETTIMESERVICESPACKAGEBYCODEPACKAGECODEPARAMETER_NTP_STANDARD), + string(fabricv4.GETTIMESERVICESPACKAGEBYCODEPACKAGECODEPARAMETER_NTP_ENTERPRISE), + string(fabricv4.GETTIMESERVICESPACKAGEBYCODEPACKAGECODEPARAMETER_PTP_STANDARD), + string(fabricv4.GETTIMESERVICESPACKAGEBYCODEPACKAGECODEPARAMETER_PTP_ENTERPRISE), + ), + }, + }, + "href": schema.StringAttribute{ + Description: "Time Precision Package HREF link to corresponding resource in Equinix Portal", + Optional: true, + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Type of the Precision Time Service Package", + Computed: true, + }, + "bandwidth": schema.Int64Attribute{ + Description: "Bandwidth of the Precision Time Service", + Computed: true, + }, + "clients_per_second_max": schema.Int64Attribute{ + Description: "Maximum clients available per second for the Precision Time Service", + Computed: true, + }, + "redundancy_supported": schema.BoolAttribute{ + Description: "Boolean flag indicating if this Precision Time Service supports redundancy", + Computed: true, + }, + "multi_subnet_supported": schema.BoolAttribute{ + Description: "Boolean flag indicating if this Precision Time Service supports multi subnetting", + Computed: true, + }, + "accuracy_unit": schema.StringAttribute{ + Description: "Time unit of accuracy for the Precision Time Service; e.g. microseconds", + Computed: true, + }, + "accuracy_sla": schema.Int64Attribute{ + Description: "SLA for the accuracy provided by the Precision Time Service", + Computed: true, + }, + "accuracy_avg_min": schema.Int64Attribute{ + Description: "Average minimum accuracy provided by the Precision Time Service", + Computed: true, + }, + "accuracy_avg_max": schema.Int64Attribute{ + Description: "Average maximum accuracy provided by the Precision Time Service", + Computed: true, + }, + }, + }, + "ipv4": schema.SingleNestedBlock{ + Description: "An object that has Network IP Configurations for Timing Master Servers.", + CustomType: fwtypes.NewObjectTypeOf[Ipv4Model](ctx), + Attributes: map[string]schema.Attribute{ + "primary": schema.StringAttribute{ + Description: "IPv4 address for the Primary Timing Master Server.", + Required: true, + }, + "secondary": schema.StringAttribute{ + Description: "IPv4 address for the Secondary Timing Master Server.", + Required: true, + }, + "network_mask": schema.StringAttribute{ + Description: "IPv4 address that defines the range of consecutive subnets in the network.", + Required: true, + }, + "default_gateway": schema.StringAttribute{ + Description: "IPv4 address that establishes the Routing Interface where traffic is directed. It serves as the next hop in the Network.", + Required: true, + }, + }, + }, + "connections": schema.ListNestedBlock{ + Description: "An array of objects with unique identifiers of connections.", + CustomType: fwtypes.NewListNestedObjectTypeOf[ConnectionModel](ctx), + NestedObject: schema.NestedBlockObject{ + CustomType: fwtypes.NewObjectTypeOf[ConnectionModel](ctx), + Attributes: map[string]schema.Attribute{ + "uuid": schema.StringAttribute{ + Description: "Equinix Fabric Connection UUID; Precision Time Service will be connected with it", + Required: true, + }, + "href": schema.StringAttribute{ + Description: "Link to the Equinix Fabric Connection associated with the Precision Time Service", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Type of the Equinix Fabric Connection associated with the Precision Time Service", + Computed: true, + }, + }, + }, + }, + }, + } +} diff --git a/internal/resources/fabric/precision_time/resource_test.go b/internal/resources/fabric/precision_time/resource_test.go new file mode 100644 index 000000000..7904076b6 --- /dev/null +++ b/internal/resources/fabric/precision_time/resource_test.go @@ -0,0 +1,268 @@ +package precision_time + +import ( + "fmt" + "testing" + + "github.com/equinix/terraform-provider-equinix/internal/acceptance" + "github.com/equinix/terraform-provider-equinix/internal/fabric/testing_helpers" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccFabricCreatePort2EPT_NTPConfiguration_PFCR(t *testing.T) { + ports := testing_helpers.GetFabricEnvPorts(t) + var portUuid string + if len(ports) > 0 { + portUuid = ports["pfcr"]["dot1q"][0].GetUuid() + } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t); acceptance.TestAccPreCheckProviderConfigured(t) }, + Providers: acceptance.TestAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccFabricCreatePort2EPTNPTConfig("NTP Configured Precision Time Service", "port2eptntp_PFCR", portUuid, "SV"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("equinix_fabric_precision_time.ntp", "id"), + resource.TestCheckResourceAttrSet("equinix_fabric_precision_time.ntp", "connections.0.uuid"), + resource.TestCheckResourceAttrSet("equinix_fabric_precision_time.ntp", "project_id"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ntp", "name", "tf_acc_eptntp_PFCR"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ntp", "type", "NTP"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ntp", "package.code", "PTP_STANDARD"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ntp", "ipv4.primary", "192.168.254.241"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ntp", "ipv4.secondary", "192.168.254.242"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ntp", "ipv4.network_mask", "255.255.255.240"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ntp", "ipv4.default_gateway", "192.168.254.254"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_precision_time.ntp", "id"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_precision_time.ntp", "connections.0.uuid"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_precision_time.ntp", "project_id"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ntp", "name", "tf_acc_eptntp_PFCR"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ntp", "type", "NTP"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ntp", "package.code", "PTP_STANDARD"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ntp", "ipv4.primary", "192.168.254.241"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ntp", "ipv4.secondary", "192.168.254.242"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ntp", "ipv4.network_mask", "255.255.255.240"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ntp", "ipv4.default_gateway", "192.168.254.254"), + ), + ExpectNonEmptyPlan: false, + }, + }, + }) + +} + +func testAccFabricCreatePort2EPTNPTConfig(spName, name, portUuid, zSideMetro string) string { + return fmt.Sprintf(` + + data "equinix_fabric_service_profiles" "this" { + filter { + property = "/name" + operator = "=" + values = ["%s"] + } + } + + + resource "equinix_fabric_connection" "test" { + name = "%s" + type = "EVPL_VC" + notifications{ + type="ALL" + emails=["example@equinix.com"] + } + bandwidth = 1 + redundancy {priority= "PRIMARY"} + order { + purchase_order_number= "1-323292" + } + a_side { + access_point { + type= "COLO" + port { + uuid= "%s" + } + link_protocol { + type= "DOT1Q" + vlan_tag= "1354" + } + } + } + z_side { + access_point { + type= "SP" + profile { + type= "L2_PROFILE" + uuid= data.equinix_fabric_service_profiles.this.data.0.uuid + } + location { + metro_code= "%s" + } + } + } + } + + resource "equinix_fabric_precision_time" "ntp" { + type = "NTP" + name = "tf_acc_eptntp_PFCR" + package { + code = "NTP_STANDARD" + } + connections { + uuid = equinix_fabric_connection.test.id + } + ipv4 { + primary = "192.168.254.241" + secondary = "192.168.254.242" + network_mask = "255.255.255.240" + default_gateway = "192.168.254.254" + } + } + + data "equinix_fabric_precision_time" "ntp" { + uuid = equinix_precision_time.ntp.id + } + +`, spName, name, portUuid, zSideMetro) +} + +func TestAccFabricCreatePort2EPT_PTPConfiguration_PFCR(t *testing.T) { + ports := testing_helpers.GetFabricEnvPorts(t) + var portUuid string + if len(ports) > 0 { + portUuid = ports["pfcr"]["dot1q"][0].GetUuid() + } + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acceptance.TestAccPreCheck(t); acceptance.TestAccPreCheckProviderConfigured(t) }, + Providers: acceptance.TestAccProviders, + Steps: []resource.TestStep{ + { + Config: testAccFabricCreatePort2EPTPTPConfig("PTP Configured Precision Time Service", "port2eptptp_PFCR", portUuid, "SV"), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrSet("equinix_fabric_precision_time.ptp", "id"), + resource.TestCheckResourceAttrSet("equinix_fabric_precision_time.ptp", "connections.0.uuid"), + resource.TestCheckResourceAttrSet("equinix_fabric_precision_time.ptp", "project_id"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ptp", "name", "tf_acc_eptptp_PFCR"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ptp", "type", "PTP"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ptp", "package.code", "PTP_STANDARD"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ptp", "ipv4.primary", "192.168.254.241"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ptp", "ipv4.secondary", "192.168.254.242"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ptp", "ipv4.network_mask", "255.255.255.240"), + resource.TestCheckResourceAttr( + "equinix_fabric_precision_time.ptp", "ipv4.default_gateway", "192.168.254.254"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_precision_time.ptp", "id"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_precision_time.ptp", "connections.0.uuid"), + resource.TestCheckResourceAttrSet("data.equinix_fabric_precision_time.ptp", "project_id"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ptp", "name", "tf_acc_eptptp_PFCR"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ptp", "type", "PTP"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ptp", "package.code", "PTP_STANDARD"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ptp", "ipv4.primary", "192.168.254.241"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ptp", "ipv4.secondary", "192.168.254.242"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ptp", "ipv4.network_mask", "255.255.255.240"), + resource.TestCheckResourceAttr( + "data.equinix_fabric_precision_time.ptp", "ipv4.default_gateway", "192.168.254.254"), + ), + ExpectNonEmptyPlan: false, + }, + }, + }) + +} + +func testAccFabricCreatePort2EPTPTPConfig(spName, name, portUuid, zSideMetro string) string { + return fmt.Sprintf(` + + data "equinix_fabric_service_profiles" "this" { + filter { + property = "/name" + operator = "=" + values = ["%s"] + } + } + + + resource "equinix_fabric_connection" "test" { + name = "%s" + type = "EVPL_VC" + notifications{ + type="ALL" + emails=["example@equinix.com"] + } + bandwidth = 5 + redundancy {priority= "PRIMARY"} + order { + purchase_order_number= "1-323292" + } + a_side { + access_point { + type= "COLO" + port { + uuid= "%s" + } + link_protocol { + type= "DOT1Q" + vlan_tag= "1355" + } + } + } + z_side { + access_point { + type= "SP" + profile { + type= "L2_PROFILE" + uuid= data.equinix_fabric_service_profiles.this.data.0.uuid + } + location { + metro_code= "%s" + } + } + } + } + + resource "equinix_fabric_precision_time" "ptp" { + type = "PTP" + name = "tf_acc_eptptp_PFCR" + package { + code = "PTP_STANDARD" + } + connections { + uuid = equinix_fabric_connection.test.id + } + ipv4 { + primary = "192.168.254.241" + secondary = "192.168.254.242" + network_mask = "255.255.255.240" + default_gateway = "192.168.254.254" + } + } + + data "equinix_fabric_precision_time" "ptp" { + uuid = equinix_precision_time.ptp.id + } + +`, spName, name, portUuid, zSideMetro) +} diff --git a/templates/data-sources/fabric_precision_time.md.tmpl b/templates/data-sources/fabric_precision_time.md.tmpl new file mode 100644 index 000000000..bc9cf2826 --- /dev/null +++ b/templates/data-sources/fabric_precision_time.md.tmpl @@ -0,0 +1,22 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "equinix_fabric_precision_time Data Source - terraform-provider-equinix" +subcategory: "Fabric" +description: |- + Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Precision Time Service by Uuid +--- + +# equinix_fabric_precision_time (Data Source) + +Fabric V4 API compatible data resource that allow user to fetch Equinix Fabric Precision Time Service by Uuid + +Additional documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Edge-Services/EPT/EPT.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#precision-time + +## Example Usage + +{{tffile "examples/data-sources/fabric_precision_time/get_by_uuid.tf"}} + + +{{ .SchemaMarkdown }} diff --git a/templates/resources/fabric_precision_time.md.tmpl b/templates/resources/fabric_precision_time.md.tmpl new file mode 100644 index 000000000..c4eb3f0c0 --- /dev/null +++ b/templates/resources/fabric_precision_time.md.tmpl @@ -0,0 +1,25 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "equinix_fabric_precision_time Resource - terraform-provider-equinix" +subcategory: "Fabric" +description: |- + Fabric V4 API compatible resource allows creation and management of Equinix Fabric Precision Time +--- + +# equinix_fabric_precision_time (Resource) + +Fabric V4 API compatible resource allows creation and management of Equinix Fabric Precision Time Services + +Additional documentation: +* Getting Started: https://docs.equinix.com/en-us/Content/Edge-Services/EPT/EPT.htm +* API: https://developer.equinix.com/dev-docs/fabric/api-reference/fabric-v4-apis#precision-time + +## Example Usage + +PTP Configuration: +{{tffile "examples/resources/fabric_precision_time/ptp_time_service.tf"}} + +NTP Configuration: +{{tffile "examples/resources/fabric_precision_time/ntp_time_service.tf"}} + +{{ .SchemaMarkdown }}