diff --git a/community/cloud-foundation/templates/cloud_nat/README.md b/community/cloud-foundation/templates/cloud_nat/README.md new file mode 100644 index 000000000..0c33ed92a --- /dev/null +++ b/community/cloud-foundation/templates/cloud_nat/README.md @@ -0,0 +1,64 @@ +# Cloud NAT + +This template creates a Cloud NAT. + +## Prerequisites + +- Install [gcloud](https://cloud.google.com/sdk) +- Create a [GCP project, set up billing, enable requisite APIs](../project/README.md) +- Create a [network](../network/README.md) +- Grant the [compute.networkAdmin](https://cloud.google.com/compute/docs/access/iam) IAM role to the project service account + +## Deployment + +### Resources + +- [compute.v1.router](https://cloud.google.com/compute/docs/reference/rest/beta/routers) + +### Properties + +See the `properties` section in the schema file(s): +- [Cloud NAT](cloud_nat.py.schema) + +### Usage + +1. Clone the [Cloud Foundation Toolkit Template repository](https://github.com/GoogleCloudPlatform/deploymentmanager-samples): + +``` + git clone https://github.com/GoogleCloudPlatform/deploymentmanager-samples.git +``` + +2. Go to the [templates](templates/) directory: + +``` + cd templates +``` + +3. Copy the example DM config to be used as a model for the deployment; in this case, [examples/cloud_nat.yaml](examples/cloud_nat.yaml): + +``` + cp templates/cloud_nat/examples/cloud_nat.yaml my_cloud_nat.yaml +``` + +4. Change the values in the config file to match your specific GCP setup (for properties, refer to the schema files listed above): + +``` + vim my_cloud_nat.yaml # <== change values to match your GCP setup +``` + +5. Create your deployment (replace with the relevant deployment name): + +``` + gcloud deployment-manager deployments create \ + --config my_cloud_nat.yaml +``` + +6. In case you need to delete your deployment: + +``` + gcloud deployment-manager deployments delete +``` + +## Examples + +- [Cloud NAT](examples/cloud_nat.yaml) diff --git a/community/cloud-foundation/templates/cloud_nat/cloud_nat.py b/community/cloud-foundation/templates/cloud_nat/cloud_nat.py new file mode 100644 index 000000000..525acea4f --- /dev/null +++ b/community/cloud-foundation/templates/cloud_nat/cloud_nat.py @@ -0,0 +1,98 @@ +""" This template creates a Cloud NAT. """ +MIN_PORTS_PER_VM = 64 +UDP_IDLE_TIMEOUT_SEC = 30 +ICMP_IDLE_TIMEOUT_SEC = 30 +TCP_ESTABLISHED_IDLE_TIMEOUT_SEC = 1200 +TCP_TRANSITORY_IDLE_TIMEOUT_SEC = 30 + +def get_nat_ip_option_enum(natips): + """ Returns one of the two supported Enum for Nat IP Option + AUTO_ONLY or MANUAL_ONLY """ + if natips: + return 'MANUAL_ONLY' + return 'AUTO_ONLY' + +def generate_config(context): + """ Entry point for the deployment resources. """ + + name = context.properties.get('name', context.env['name']) + + resources = [ + { + 'name': context.env['name'], + # compute.v1.router seems to be doing the job though + # doc says compute.beta has the NAT feature. + 'type': 'compute.v1.router', + 'properties': + { + 'name': + name, + 'network': + generate_network_url( + context, + context.properties['network'] + ), + 'region': + context.properties['region'], + 'nats': + [{ + 'name': name, + # Force using All subnet all primary IP range by default + # Force using one of the two enums below: + # ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES and + # ALL_SUBNETWORKS_ALL_IP_RANGES + 'sourceSubnetworkIpRangesToNat': context.properties.get( + 'sourceSubnetworkIpRangesToNat', + 'ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES'), + 'natIps': context.properties.get('natIps', []), + 'natIpAllocateOption': get_nat_ip_option_enum( + context.properties.get('natIps')), + # A min of 64, anything below will + # still be translated to 64 ports + 'minPortsPerVm': context.properties.get( + 'minPortsPerVm', MIN_PORTS_PER_VM), + 'udpIdleTimeoutSec': context.properties.get( + 'udpIdleTimeoutSec', UDP_IDLE_TIMEOUT_SEC), + 'icmpIdleTimeoutSec': context.properties.get( + 'icmpIdleTimeoutSec', ICMP_IDLE_TIMEOUT_SEC), + 'tcpEstablishedIdleTimeoutSec': context.properties.get( + 'tcpEstablishedIdleTimeoutSec', + TCP_ESTABLISHED_IDLE_TIMEOUT_SEC), + 'tcpTransitoryIdleTimeoutSec': context.properties.get( + 'tcpTransitoryIdleTimeoutSec', + TCP_TRANSITORY_IDLE_TIMEOUT_SEC) + }] + } + } + ] + + return { + 'resources': + resources, + 'outputs': + [ + { + 'name': 'name', + 'value': name + }, + { + 'name': 'selfLink', + 'value': '$(ref.' + context.env['name'] + '.selfLink)' + }, + { + 'name': + 'nats', + 'value': + '$(ref.' + context.env['name'] + '.nats)' + } + ] + } + + +def generate_network_url(context, network): + """Format the resource name as a resource URI.""" + + return 'projects/{}/global/networks/{}'.format( + context.env['project'], + network + ) diff --git a/community/cloud-foundation/templates/cloud_nat/cloud_nat.py.schema b/community/cloud-foundation/templates/cloud_nat/cloud_nat.py.schema new file mode 100644 index 000000000..e0abc3b20 --- /dev/null +++ b/community/cloud-foundation/templates/cloud_nat/cloud_nat.py.schema @@ -0,0 +1,81 @@ +info: + title: Cloud Nat + author: nan.jiang@sourcedgroup.com + description: | + Deploys a Cloud Nat. + + For more information on this resource: + https://cloud.google.com/nat/docs/overview + +imports: + - path: cloud_nat.py + +required: + - network + - region + - name + - sourceSubnetworkIpRangesToNat + +properties: + name: + type: string + description: The name of Cloud Nat the resource. + network: + type: string + description: The name of the network to which the Cloud Nat belongs. + region: + type: string + description: The URI of the region where the Cloud Nat resides. + sourceSubnetworkIpRangesToNat: + type: string + description: IP Range inside the VPC effected by the nats service. + enum: + - ALL_SUBNETWORKS_ALL_IP_RANGES + - ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES + minPortsPerVm: + type: number + description: | + Minimum number of ports allocated to a VM from this NAT config, + any setting belong 64 will be automatically converted to 64 + udpIdleTimeoutSec: + type: number + description: Timeout for UDP connections in seconds + default: 30 + icmpIdleTimeoutSec: + type: number + description: Timeout for ICMP connections in seconds + default: 30 + tcpEstablishedIdleTimeoutSec: + type: number + description: Timeout for established TCP connections in seconds + default: 1200 + tcpTransitoryIdleTimeoutSec: + type: number + description: Timeout for TCP connections in seconds + default: 30 + natIps: + type: array + description: Static External IPs used as the NAT IPs. + items: + type: string + description: Static External IP self link + +outputs: + properties: + - name: + type: string + description: The name of the Cloud Nat resource. + - selfLink: + type: string + description: The URI (SelfLink) of the Cloud Nat resource. + - nat: + type: object + description: | + Object containing NAT information. + https://cloud.google.com/compute/docs/reference/rest/beta/routers + +documentation: + - templates/cloud_nat/README.md + +examples: + - templates/cloud_nat/examples/cloud_nat.yaml diff --git a/community/cloud-foundation/templates/cloud_nat/examples/cloud_nat.yaml b/community/cloud-foundation/templates/cloud_nat/examples/cloud_nat.yaml new file mode 100644 index 000000000..617730a97 --- /dev/null +++ b/community/cloud-foundation/templates/cloud_nat/examples/cloud_nat.yaml @@ -0,0 +1,22 @@ +# Example of the Cloud NAT template usage. +# Replace with your resource names + +imports: + - path: templates/cloud_nat/cloud_nat.py + name: cloud_nat.py + +resources: + - name: cloud-nat + type: cloud_nat.py + properties: + name: nat + network: + region: us-east1 + sourceSubnetworkIpRangesToNat: ALL_SUBNETWORKS_ALL_IP_RANGES + minPortsPerVm: 64 + udpIdleTimeoutSec: 30 + icmpIdleTimeoutSec: 30 + tcpEstablishedIdleTimeoutSec: 1200 + tcpTransitoryIdleTimeoutSec: 30 + natIps: + - projects//regions/us-east1/addresses/ \ No newline at end of file diff --git a/community/cloud-foundation/templates/cloud_nat/tests/integration/cloud_nat.bats b/community/cloud-foundation/templates/cloud_nat/tests/integration/cloud_nat.bats new file mode 100755 index 000000000..11ed69a0c --- /dev/null +++ b/community/cloud-foundation/templates/cloud_nat/tests/integration/cloud_nat.bats @@ -0,0 +1,92 @@ +#!/usr/bin/env bats + +source tests/helpers.bash + +TEST_NAME=$(basename "${BATS_TEST_FILENAME}" | cut -d '.' -f 1) + +# Create a random 10-char string and save it in a file. +RANDOM_FILE="/tmp/${CLOUD_FOUNDATION_ORGANIZATION_ID}-${TEST_NAME}.txt" +if [[ ! -e "${RANDOM_FILE}" ]]; then + RAND=$(head /dev/urandom | LC_ALL=C tr -dc a-z0-9 | head -c 10) + echo ${RAND} > "${RANDOM_FILE}" +fi + +# Set variables based on the random string saved in the file. +# envsubst requires all variables used in the example/config to be exported. +if [[ -e "${RANDOM_FILE}" ]]; then + export RAND=$(cat "${RANDOM_FILE}") + DEPLOYMENT_NAME="${CLOUD_FOUNDATION_PROJECT_ID}-${TEST_NAME}-${RAND}" + # Replace underscores with dashes in the deployment name. + DEPLOYMENT_NAME=${DEPLOYMENT_NAME//_/-} + CONFIG=".${DEPLOYMENT_NAME}.yaml" +fi + +########## HELPER FUNCTIONS ########## + +function create_config() { + echo "Creating ${CONFIG}" + envsubst < "templates/cloud_nat/tests/integration/${TEST_NAME}.yaml" > "${CONFIG}" +} + +function delete_config() { + echo "Deleting ${CONFIG}" + rm -f "${CONFIG}" +} + +function setup() { + # Global setup; executed once per test file. + if [ ${BATS_TEST_NUMBER} -eq 1 ]; then + gcloud compute networks create network-${RAND} \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" \ + --description "integration test ${RAND}" \ + --subnet-mode custom + gcloud compute addresses create ip-${RAND} \ + --region us-east1 \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + create_config + fi + + # Per-test setup steps. + } + +function teardown() { + # Global teardown; executed once per test file. + if [[ "$BATS_TEST_NUMBER" -eq "${#BATS_TEST_NAMES[@]}" ]]; then + gcloud compute networks delete network-${RAND} \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" -q + gcloud compute addresses delete ip-${RAND} -q --region us-east1 + rm -f "${RANDOM_FILE}" + delete_config + fi + + # Per-test teardown steps. +} + + +@test "Creating deployment ${DEPLOYMENT_NAME} from ${CONFIG}" { + gcloud deployment-manager deployments create "${DEPLOYMENT_NAME}" \ + --config ${CONFIG} \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" +} + +@test "Verifying that NATS were created in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute routers nats describe nat-"${RAND}" --router=cloud-nat-"${RAND}" --region=us-east1 --project "${CLOUD_FOUNDATION_PROJECT_ID}" + echo '---Verification Output Start---' + echo "$output" + echo '---Verification Output Complete---' + [[ "$output" =~ "icmpIdleTimeoutSec: 60" ]] + [[ "$output" =~ "minPortsPerVm: 96" ]] + [[ "$output" =~ "name: nat-${RAND}" ]] + [[ "$output" =~ "tcpEstablishedIdleTimeoutSec: 1140" ]] + [[ "$output" =~ "tcpTransitoryIdleTimeoutSec: 60" ]] + [[ "$output" =~ "udpIdleTimeoutSec: 55" ]] + [[ "$output" =~ "/regions/us-east1/addresses/ip-${RAND}" ]] +} + +@test "Deleting deployment" { + gcloud deployment-manager deployments delete "${DEPLOYMENT_NAME}" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" -q + + run gcloud compute nats list --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ ! "$output" =~ "name: cloud-nat-${RAND}" ]] +} diff --git a/community/cloud-foundation/templates/cloud_nat/tests/integration/cloud_nat.yaml b/community/cloud-foundation/templates/cloud_nat/tests/integration/cloud_nat.yaml new file mode 100644 index 000000000..13eae09f4 --- /dev/null +++ b/community/cloud-foundation/templates/cloud_nat/tests/integration/cloud_nat.yaml @@ -0,0 +1,26 @@ +# Test of the Cloud Router template usage. +# +# Variables: +# RAND: a random string used by the testing suite. +# + +imports: + - path: templates/cloud_nat/cloud_nat.py + name: cloud_nat.py + +resources: + - name: cloud-nat-${RAND} + type: cloud_nat.py + properties: + name: nat-${RAND} + network: network-${RAND} + region: us-east1 + # ALL_SUBNETWORKS_ALL_PRIMARY_IP_RANGES + sourceSubnetworkIpRangesToNat: ALL_SUBNETWORKS_ALL_IP_RANGES + minPortsPerVm: 96 + udpIdleTimeoutSec: 55 + icmpIdleTimeoutSec: 60 + tcpEstablishedIdleTimeoutSec: 1140 + tcpTransitoryIdleTimeoutSec: 60 + natIps: + - projects/${CLOUD_FOUNDATION_PROJECT_ID}/regions/us-east1/addresses/ip-${RAND}