diff --git a/modules/.DS_Store b/modules/.DS_Store deleted file mode 100644 index e0944c74..00000000 Binary files a/modules/.DS_Store and /dev/null differ diff --git a/modules/bloxone_anycast/.terraform-docs.yml b/modules/bloxone_anycast/.terraform-docs.yml new file mode 100644 index 00000000..5f4a72dd --- /dev/null +++ b/modules/bloxone_anycast/.terraform-docs.yml @@ -0,0 +1,12 @@ +formatter: "markdown table" + +header-from: main.tf + +sections: + hide: + - modules + +output: + file: "README.md" + mode: inject + diff --git a/modules/bloxone_anycast/README.md b/modules/bloxone_anycast/README.md new file mode 100644 index 00000000..a9e2b288 --- /dev/null +++ b/modules/bloxone_anycast/README.md @@ -0,0 +1,212 @@ + +# Terraform Module to Create BloxOne Anycast Configurations + +This Terraform module configures BloxOne Anycast services for DHCP HA pairs, DNS, and DFP based on the specified service type. It fetches BloxOne hosts by provided names, creates an anycast configuration profile with the desired IP address and routing protocols, and if the service type is `dhcp`, it also creates a DHCP HA group. + +Note: The module only creates the `anycast` service object and assumes pre-existing hosts in BloxOne and pre-configured `dhcp`, `dns`, or `dfp` services. +## Example Usage + +### Anycast Configuration for DHCP +```hcl +module "bloxone_anycast" { + anycast_config_name = "ac" + + hosts = { + host1 = { + ha_role = "active", + routing_protocols = ["BGP", "OSPF"] + bgp_config = { + asn = "65001" + holddown_secs = 180 + neighbors = [ + { asn = "65002", ip_address = "172.28.4.198" } + ] + } + ospf_config = { + area = "0.0.0.0" + area_type = "STANDARD" + authentication_type = "Clear" + authentication_key = "YXV0aGVk" + interface = "ens5" + hello_interval = 10 + dead_interval = 40 + retransmit_interval = 5 + transmit_delay = 1 + } + }, + host2 = { + ha_role = "passive", + routing_protocols = ["OSPF"] + ospf_config = { + area = "0.0.0.1" + area_type = "STANDARD" + authentication_type = "Clear" + authentication_key = "YXV0aGVk" + interface = "ens5" + hello_interval = 10 + dead_interval = 40 + retransmit_interval = 5 + transmit_delay = 1 + } + } + } + + service = "dhcp" + anycast_ip_address = "192.2.2.1" + ha_group_name = "example_ha_group" + } +``` +### Anycast Configuration for DNS +```hcl + + module "bloxone_anycast" { + anycast_config_name = "ac" + + hosts = { + host1 = { + routing_protocols = ["BGP", "OSPF"] + bgp_config = { + asn = "65001" + holddown_secs = 180 + neighbors = [ + { asn = "65002", ip_address = "172.28.4.198" } + ] + } + ospf_config = { + area = "0.0.0.0" + area_type = "STANDARD" + authentication_type = "Clear" + authentication_key = "YXV0aGVk" + interface = "ens5" + hello_interval = 10 + dead_interval = 40 + retransmit_interval = 5 + transmit_delay = 1 + } + }, + host2 = { + routing_protocols = ["OSPF"] + ospf_config = { + area = "0.0.0.1" + area_type = "STANDARD" + authentication_type = "Clear" + authentication_key = "YXV0aGVk" + interface = "ens5" + hello_interval = 10 + dead_interval = 40 + retransmit_interval = 5 + transmit_delay = 1 + } + } + } + + service = "dns" + anycast_ip_address = "192.2.2.1" + } +``` +### Anycast Configuration for DFP +```hcl +module "bloxone_anycast" { + anycast_config_name = "ac" + + hosts = { + host1 = { + routing_protocols = ["BGP", "OSPF"] + bgp_config = { + asn = "65001" + holddown_secs = 180 + neighbors = [ + { asn = "65002", ip_address = "172.28.4.198" } + ] + } + ospf_config = { + area = "0.0.0.0" + area_type = "STANDARD" + authentication_type = "Clear" + authentication_key = "YXV0aGVk" + interface = "ens5" + hello_interval = 10 + dead_interval = 40 + retransmit_interval = 5 + transmit_delay = 1 + } + }, + host2 = { + routing_protocols = ["OSPF"] + ospf_config = { + area = "0.0.0.1" + area_type = "STANDARD" + authentication_type = "Clear" + authentication_key = "YXV0aGVk" + interface = "ens5" + hello_interval = 10 + dead_interval = 40 + retransmit_interval = 5 + transmit_delay = 1 + } + } + host3 = { + role = "passive", + routing_protocols = ["OSPF"] + ospf_config = { + area = "0.0.0.1" + area_type = "STANDARD" + authentication_type = "Clear" + authentication_key = "YXV0aGVk" + interface = "ens5" + hello_interval = 10 + dead_interval = 40 + retransmit_interval = 5 + transmit_delay = 1 + } + } + } + service = "dfp" + anycast_ip_address = "192.2.2.1" +} +``` + +## Requirements + +| Name | Version | +|------|---------| +| [bloxone](#requirement\_bloxone) | >= 1.1.0 | + +## Providers + +| Name | Version | +|------|---------| +| [bloxone](#provider\_bloxone) | >= 1.1.0 | + +## Resources + +| Name | Type | +|------|------| +| [bloxone_anycast_config.ac](https://registry.terraform.io/providers/infobloxopen/bloxone/latest/docs/resources/anycast_config) | resource | +| [bloxone_anycast_host.this](https://registry.terraform.io/providers/infobloxopen/bloxone/latest/docs/resources/anycast_host) | resource | +| [bloxone_dhcp_ha_group.this](https://registry.terraform.io/providers/infobloxopen/bloxone/latest/docs/resources/dhcp_ha_group) | resource | +| [bloxone_infra_service.anycast](https://registry.terraform.io/providers/infobloxopen/bloxone/latest/docs/resources/infra_service) | resource | +| [bloxone_dhcp_hosts.this](https://registry.terraform.io/providers/infobloxopen/bloxone/latest/docs/data-sources/dhcp_hosts) | data source | +| [bloxone_infra_hosts.this](https://registry.terraform.io/providers/infobloxopen/bloxone/latest/docs/data-sources/infra_hosts) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [anycast\_config\_name](#input\_anycast\_config\_name) | Name of the Anycast configuration. | `string` | n/a | yes | +| [anycast\_ip\_address](#input\_anycast\_ip\_address) | Anycast IP address. | `string` | n/a | yes | +| [ha\_group\_name](#input\_ha\_group\_name) | Name of the HA group. | `string` | `null` | no | +| [hosts](#input\_hosts) | Map of hostnames with their roles, routing protocols, BGP, and OSPF configurations. |
map(object({| `{}` | no | +| [service](#input\_service) | The type of the Service used in anycast configuration, supports (`dns`, `dhcp`, `dfp`). | `string` | `"dhcp"` | no | +| [timeouts](#input\_timeouts) | The timeouts to use for the BloxOne Host. The timeout value is a string that can be parsed as a duration consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). If not provided, the default timeouts will be used. |
ha_role = optional(string)
routing_protocols = list(string)
bgp_config = optional(object({
asn = optional(string)
holddown_secs = optional(number)
neighbors = optional(list(object({
asn = string
ip_address = string
})))
}))
ospf_config = optional(object({
area = optional(string)
area_type = optional(string)
authentication_type = optional(string)
authentication_key = optional(string)
interface = optional(string)
hello_interval = optional(number)
dead_interval = optional(number)
retransmit_interval = optional(number)
transmit_delay = optional(number)
}))
}))
object({| `null` | no | +| [wait\_for\_state](#input\_wait\_for\_state) | If set to `true`, the resource will wait for the desired state to be reached before returning. If set to `false`, the resource will return immediately after the request is sent to the API. | `bool` | `true` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [anycast\_config](#output\_anycast\_config) | The anycast config | +| [anycast\_hosts](#output\_anycast\_hosts) | Map of anycast hosts | +| [dhcp\_ha\_group](#output\_dhcp\_ha\_group) | The DHCP HA group | +| [infra\_services](#output\_infra\_services) | Map of infrastructure services | + diff --git a/modules/bloxone_anycast/main.tf b/modules/bloxone_anycast/main.tf new file mode 100644 index 00000000..8b7c83f5 --- /dev/null +++ b/modules/bloxone_anycast/main.tf @@ -0,0 +1,272 @@ +/** + * # Terraform Module to Create BloxOne Anycast Configurations + * + * This Terraform module configures BloxOne Anycast services for DHCP HA pairs, DNS, and DFP based on the specified service type. It fetches BloxOne hosts by provided names, creates an anycast configuration profile with the desired IP address and routing protocols, and if the service type is `dhcp`, it also creates a DHCP HA group. + * + * Note: The module only creates the `anycast` service object and assumes pre-existing hosts in BloxOne and pre-configured `dhcp`, `dns`, or `dfp` services. + * ## Example Usage + * + * ### Anycast Configuration for DHCP + * ```hcl + * module "bloxone_anycast" { + * anycast_config_name = "ac" + * + * hosts = { + * host1 = { + * ha_role = "active", + * routing_protocols = ["BGP", "OSPF"] + * bgp_config = { + * asn = "65001" + * holddown_secs = 180 + * neighbors = [ + * { asn = "65002", ip_address = "172.28.4.198" } + * ] + * } + * ospf_config = { + * area = "0.0.0.0" + * area_type = "STANDARD" + * authentication_type = "Clear" + * authentication_key = "YXV0aGVk" + * interface = "ens5" + * hello_interval = 10 + * dead_interval = 40 + * retransmit_interval = 5 + * transmit_delay = 1 + * } + * }, + * host2 = { + * ha_role = "passive", + * routing_protocols = ["OSPF"] + * ospf_config = { + * area = "0.0.0.1" + * area_type = "STANDARD" + * authentication_type = "Clear" + * authentication_key = "YXV0aGVk" + * interface = "ens5" + * hello_interval = 10 + * dead_interval = 40 + * retransmit_interval = 5 + * transmit_delay = 1 + * } + * } + * } + * + * service = "dhcp" + * anycast_ip_address = "192.2.2.1" + * ha_group_name = "example_ha_group" + * } + * ``` + * ### Anycast Configuration for DNS + * ```hcl + * + * module "bloxone_anycast" { + * anycast_config_name = "ac" + * + * hosts = { + * host1 = { + * routing_protocols = ["BGP", "OSPF"] + * bgp_config = { + * asn = "65001" + * holddown_secs = 180 + * neighbors = [ + * { asn = "65002", ip_address = "172.28.4.198" } + * ] + * } + * ospf_config = { + * area = "0.0.0.0" + * area_type = "STANDARD" + * authentication_type = "Clear" + * authentication_key = "YXV0aGVk" + * interface = "ens5" + * hello_interval = 10 + * dead_interval = 40 + * retransmit_interval = 5 + * transmit_delay = 1 + * } + * }, + * host2 = { + * routing_protocols = ["OSPF"] + * ospf_config = { + * area = "0.0.0.1" + * area_type = "STANDARD" + * authentication_type = "Clear" + * authentication_key = "YXV0aGVk" + * interface = "ens5" + * hello_interval = 10 + * dead_interval = 40 + * retransmit_interval = 5 + * transmit_delay = 1 + * } + * } + * } + * + * service = "dns" + * anycast_ip_address = "192.2.2.1" + * } + * ``` + * ### Anycast Configuration for DFP + * ```hcl + * module "bloxone_anycast" { + * anycast_config_name = "ac" + * + * hosts = { + * host1 = { + * routing_protocols = ["BGP", "OSPF"] + * bgp_config = { + * asn = "65001" + * holddown_secs = 180 + * neighbors = [ + * { asn = "65002", ip_address = "172.28.4.198" } + * ] + * } + * ospf_config = { + * area = "0.0.0.0" + * area_type = "STANDARD" + * authentication_type = "Clear" + * authentication_key = "YXV0aGVk" + * interface = "ens5" + * hello_interval = 10 + * dead_interval = 40 + * retransmit_interval = 5 + * transmit_delay = 1 + * } + * }, + * host2 = { + * routing_protocols = ["OSPF"] + * ospf_config = { + * area = "0.0.0.1" + * area_type = "STANDARD" + * authentication_type = "Clear" + * authentication_key = "YXV0aGVk" + * interface = "ens5" + * hello_interval = 10 + * dead_interval = 40 + * retransmit_interval = 5 + * transmit_delay = 1 + * } + * } + * host3 = { + * role = "passive", + * routing_protocols = ["OSPF"] + * ospf_config = { + * area = "0.0.0.1" + * area_type = "STANDARD" + * authentication_type = "Clear" + * authentication_key = "YXV0aGVk" + * interface = "ens5" + * hello_interval = 10 + * dead_interval = 40 + * retransmit_interval = 5 + * transmit_delay = 1 + * } + * } + * } + * service = "dfp" + * anycast_ip_address = "192.2.2.1" + * } + * ``` + */ + +locals { + service_type_to_anycast_service_type = { + "dhcp" = "DHCP" + "dns" = "DNS" + "dfp" = "DFP" + } +} + +data "bloxone_infra_hosts" "this" { + for_each = var.hosts + filters = { + "display_name" = each.key + } + + lifecycle { + postcondition { + condition = self.results != null + error_message = "Host not found for ${each.key}" + } + + postcondition { + condition = contains(self.results[0].configs[*].service_type, var.service) + error_message = "${var.service} for ${each.key} is not configured" + } + } +} + +# Create an anycast config profile with on-prem hosts +resource "bloxone_anycast_config" "ac" { + anycast_ip_address = var.anycast_ip_address + name = var.anycast_config_name + service = local.service_type_to_anycast_service_type[var.service] +} + +resource "bloxone_infra_service" "anycast" { + for_each = var.hosts + name = format("%s_anycast", each.key) + pool_id = data.bloxone_infra_hosts.this[each.key].results[0].pool_id + service_type = "anycast" + desired_state = "start" + wait_for_state = false +} + +# Adding an anycast host with BGP and OSPF routing protocol +resource "bloxone_anycast_host" "this" { + for_each = data.bloxone_infra_hosts.this + id = one(data.bloxone_infra_hosts.this[each.key].results).legacy_id + + # Adding the anycast config profile and enabling BGP routing protocol + anycast_config_refs = [ + { + anycast_config_name = bloxone_anycast_config.ac.name + routing_protocols = var.hosts[each.key].routing_protocols + } + ] + + # Adding the BGP configuration if specified + config_bgp = contains(var.hosts[each.key].routing_protocols, "BGP") && var.hosts[each.key].bgp_config != null ? var.hosts[each.key].bgp_config : null + + # Adding the OSPF configuration if specified + config_ospf = contains(var.hosts[each.key].routing_protocols, "OSPF") && var.hosts[each.key].ospf_config != null ? var.hosts[each.key].ospf_config : null +} + +data "bloxone_dhcp_hosts" "this" { + for_each = var.hosts + filters = { + name = each.key + } + + lifecycle { + postcondition { + condition = self.results != null + error_message = "Host not found for ${each.key}" + } + } +} + +# Define the HA group resource +resource "bloxone_dhcp_ha_group" "this" { + count = var.service == "dhcp" ? 1 : 0 + name = var.ha_group_name + mode = "anycast" + anycast_config_id = format("accm/ac_configs/%s", bloxone_anycast_config.ac.id) + + hosts = [ + for k, v in var.hosts : { + host = data.bloxone_dhcp_hosts.this[k].results[0].id + role = v.ha_role + } + ] + + lifecycle { + precondition { + condition = var.ha_group_name != "" + error_message = "HA group name must be provided" + } + + precondition { + condition = length(var.hosts) >= 2 + error_message = "At least two hosts are required for DHCP HA" + } + } +} diff --git a/modules/bloxone_anycast/outputs.tf b/modules/bloxone_anycast/outputs.tf new file mode 100644 index 00000000..4ed4cce6 --- /dev/null +++ b/modules/bloxone_anycast/outputs.tf @@ -0,0 +1,19 @@ +output "anycast_config" { + description = "The anycast config" + value = bloxone_anycast_config.ac +} + +output "anycast_hosts" { + description = "Map of anycast hosts" + value = bloxone_anycast_host.this +} + +output "infra_services" { + description = "Map of infrastructure services" + value = bloxone_infra_service.anycast +} + +output "dhcp_ha_group" { + description = "The DHCP HA group" + value = bloxone_dhcp_ha_group.this +} \ No newline at end of file diff --git a/modules/bloxone_anycast/variables.tf b/modules/bloxone_anycast/variables.tf new file mode 100644 index 00000000..6e27d406 --- /dev/null +++ b/modules/bloxone_anycast/variables.tf @@ -0,0 +1,65 @@ +variable "hosts" { + description = "Map of hostnames with their roles, routing protocols, BGP, and OSPF configurations." + type = map(object({ + ha_role = optional(string) + routing_protocols = list(string) + bgp_config = optional(object({ + asn = optional(string) + holddown_secs = optional(number) + neighbors = optional(list(object({ + asn = string + ip_address = string + }))) + })) + ospf_config = optional(object({ + area = optional(string) + area_type = optional(string) + authentication_type = optional(string) + authentication_key = optional(string) + interface = optional(string) + hello_interval = optional(number) + dead_interval = optional(number) + retransmit_interval = optional(number) + transmit_delay = optional(number) + })) + })) + default = {} +} + +variable "ha_group_name" { + description = "Name of the HA group." + type = string + default = null +} + +variable "service" { + description = "The type of the Service used in anycast configuration, supports (`dns`, `dhcp`, `dfp`)." + type = string + default = "dhcp" +} + +variable "anycast_ip_address" { + description = "Anycast IP address." + type = string +} + +variable "anycast_config_name" { + description = "Name of the Anycast configuration." + type = string +} + +variable "timeouts" { + description = "The timeouts to use for the BloxOne Host. The timeout value is a string that can be parsed as a duration consisting of numbers and unit suffixes, such as \"30s\" or \"2h45m\". Valid time units are \"s\" (seconds), \"m\" (minutes), \"h\" (hours). If not provided, the default timeouts will be used." + type = object({ + create = string + update = string + read = string + }) + default = null +} + +variable "wait_for_state" { + description = "If set to `true`, the resource will wait for the desired state to be reached before returning. If set to `false`, the resource will return immediately after the request is sent to the API." + type = bool + default = true +} \ No newline at end of file diff --git a/modules/bloxone_anycast/versions.tf b/modules/bloxone_anycast/versions.tf new file mode 100644 index 00000000..fa726cdc --- /dev/null +++ b/modules/bloxone_anycast/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + bloxone = { + source = "infobloxopen/bloxone" + version = ">= 1.1.0" + } + } +}
create = string
update = string
read = string
})