diff --git a/dm/templates/instance/examples/instance.yaml b/dm/templates/instance/examples/instance.yaml index d213966dc0a..3c684f806db 100644 --- a/dm/templates/instance/examples/instance.yaml +++ b/dm/templates/instance/examples/instance.yaml @@ -14,7 +14,8 @@ resources: diskImage: projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts machineType: f1-micro diskType: pd-ssd - network: default + networks: + - name: default metadata: items: - key: startup-script diff --git a/dm/templates/instance/instance.py b/dm/templates/instance/instance.py index 4bcd446fbb0..bce8c8dd6ee 100644 --- a/dm/templates/instance/instance.py +++ b/dm/templates/instance/instance.py @@ -43,35 +43,46 @@ def create_boot_disk(properties, zone, instance_name): return boot_disk -def get_network(properties): +def get_network_interfaces(properties): """ Get the configuration that connects the instance to an existing network - and assigns to it an ephemeral public IP. + and assigns to it an ephemeral public IP if specified. """ - - network_name = properties.get('network') - - if not '.' in network_name and not '/' in network_name: - network_name = 'global/networks/{}'.format(network_name) - - network_interfaces = { - 'network': network_name, - } - - if properties['hasExternalIp']: - access_configs = { - 'name': 'External NAT', - 'type': 'ONE_TO_ONE_NAT' + network_interfaces = [] + + networks = properties.get('networks', [{ + "name": properties.get('network'), + "hasExternalIp": properties.get('hasExternalIp'), + "natIP": properties.get('natIP'), + "subnetwork": properties.get('subnetwork'), + "networkIP": properties.get('networkIP'), + }]) + + for network in networks: + if not '.' in network['name'] and not '/' in network['name']: + network_name = 'global/networks/{}'.format(network['name']) + else: + network_name = network['name'] + + network_interface = { + 'network': network_name, } - if 'natIP' in properties: - access_configs['natIP'] = properties['natIP'] + if network['hasExternalIp']: + access_configs = { + 'name': 'External NAT', + 'type': 'ONE_TO_ONE_NAT' + } + + if network.get('natIP'): + access_configs['natIP'] = network['natIP'] - network_interfaces['accessConfigs'] = [access_configs] + network_interface['accessConfigs'] = [access_configs] - netif_optional_props = ['subnetwork', 'networkIP'] - for prop in netif_optional_props: - if prop in properties: - network_interfaces[prop] = properties[prop] + netif_optional_props = ['subnetwork', 'networkIP'] + for prop in netif_optional_props: + if network.get(prop): + network_interface[prop] = network[prop] + network_interfaces.append(network_interface) return network_interfaces @@ -84,7 +95,7 @@ def generate_config(context): machine_type = context.properties['machineType'] boot_disk = create_boot_disk(context.properties, zone, vm_name) - network = get_network(context.properties) + network_interfaces = get_network_interfaces(context.properties) instance = { 'name': vm_name, 'type': 'compute.v1.instance', @@ -93,7 +104,7 @@ def generate_config(context): 'machineType': 'zones/{}/machineTypes/{}'.format(zone, machine_type), 'disks': [boot_disk], - 'networkInterfaces': [network] + 'networkInterfaces': network_interfaces } } @@ -102,8 +113,8 @@ def generate_config(context): outputs = [ { - 'name': 'internalIp', - 'value': '$(ref.{}.networkInterfaces[0].networkIP)'.format(vm_name) # pylint: disable=line-too-long + 'name': 'networkInterfaces', + 'value': '$(ref.{}.networkInterfaces)'.format(vm_name) }, { 'name': 'name', @@ -115,12 +126,4 @@ def generate_config(context): } ] - if context.properties['hasExternalIp']: - outputs.append( - { - 'name': 'externalIp', - 'value': '$(ref.{}.networkInterfaces[0].accessConfigs[0].natIP)'.format(vm_name) # pylint: disable=line-too-long - } - ) - return {'resources': [instance], 'outputs': outputs} diff --git a/dm/templates/instance/instance.py.schema b/dm/templates/instance/instance.py.schema index 6df0986292a..a2923a36ec3 100644 --- a/dm/templates/instance/instance.py.schema +++ b/dm/templates/instance/instance.py.schema @@ -25,7 +25,69 @@ required: - zone - machineType - diskImage - - network + +oneOf: + - allOf: + - required: + - networks + - properties: + networks: + minItems: 1 + - not: + required: + - network + - not: + required: + - natIP + - not: + required: + - subnetwork + - not: + required: + - networkIP + - allOf: + - required: + - network + - not: + required: + - networks + +additionalProperties: false + +definitions: + hasExternalIp: + type: boolean + default: true + description: | + Defines wether the instance will use an external IP from a shared + ephemeral IP address pool. If this is set to false, the instance + will not have an external IP. + natIP: + type: string + description: | + An external IP address associated with this instance. Specify an unused + static external IP address available to the project or leave this field + undefined to use an IP from a shared ephemeral IP address pool. If you + specify a static external IP address, it must live in the same region + as the zone of the instance. + If hasExternalIp is false this field is ignored. + subnetwork: + type: string + description: | + The URL of the Subnetwork resource for this instance. If the network + resource is in legacy mode, do not provide this property. If the network + is in auto subnet mode, providing the subnetwork is optional. If the + network is in custom subnet mode, then this field should be specified. + If you specify this property, you can specify the subnetwork as a full + or partial URL. For example, the following are all valid URLs: + - https://www.googleapis.com/compute/v1/projects/project/regions/region/subnetworks/subnetwork + - regions/region/subnetworks/subnetwork + networkIP: + type: string + description: | + An IPv4 internal network address to assign to the instance for this + network interface. If not specified by the user, an unused internal IP + is assigned by the system. properties: name: @@ -36,42 +98,41 @@ properties: description: | Name of the network the instance will be connected to; e.g., 'my-custom-network' or 'default'. - zone: - type: string - description: Availability zone. E.g. 'us-central1-a' hasExternalIp: - type: boolean - default: true - description: | - Defines wether the instance will use an external IP from a shared - ephemeral IP address pool. If this is set to false, the instance - will not have an external IP. + $ref: '#/definitions/hasExternalIp' natIP: - type: string - description: | - An external IP address associated with this instance. Specify an unused - static external IP address available to the project or leave this field - undefined to use an IP from a shared ephemeral IP address pool. If you - specify a static external IP address, it must live in the same region - as the zone of the instance. - If hasExternalIp is false this field is ignored. + $ref: '#/definitions/natIP' subnetwork: - type: string - description: | - The URL of the Subnetwork resource for this instance. If the network - resource is in legacy mode, do not provide this property. If the network - is in auto subnet mode, providing the subnetwork is optional. If the - network is in custom subnet mode, then this field should be specified. - If you specify this property, you can specify the subnetwork as a full - or partial URL. For example, the following are all valid URLs: - - https://www.googleapis.com/compute/v1/projects/project/regions/region/subnetworks/subnetwork - - regions/region/subnetworks/subnetwork + $ref: '#/definitions/subnetwork' networkIP: - type: string + $ref: '#/definitions/networkIP' + networks: + type: array description: | - An IPv4 internal network address to assign to the instance for this - network interface. If not specified by the user, an unused internal IP - is assigned by the system. + Networks the instance will be connected to; + e.g., 'my-custom-network' or 'default'. + items: + type: object + additionalProperties: false + required: + - name + properties: + name: + type: string + description: | + Name of the network the instance will be connected to; + e.g., 'my-custom-network' or 'default'. + hasExternalIp: + $ref: '#/definitions/hasExternalIp' + natIP: + $ref: '#/definitions/natIP' + subnetwork: + $ref: '#/definitions/subnetwork' + networkIP: + $ref: '#/definitions/networkIP' + zone: + type: string + description: Availability zone. E.g. 'us-central1-a' tags: type: object description: | @@ -95,6 +156,7 @@ properties: See https://cloud.google.com/compute/docs/machine-types for details. canIpForward: type: boolean + default: False description: | If "True". allows the instance to send and receive packets with non-matching destination and source IPs. @@ -107,6 +169,7 @@ properties: - local-ssd diskImage: type: string + default: None description: | The source image for the disk. To create the disk with one of the public operating system images, specify the image by its family name. @@ -134,6 +197,7 @@ properties: description: A collection of metadata key-value pairs. items: type: object + additionalProperties: false properties: key: type: string @@ -146,6 +210,7 @@ properties: this instance. Only one service account per VM instance is supported. items: type: object + additionalProperties: false properties: email: type: string @@ -162,12 +227,19 @@ properties: outputs: properties: - - externalIp: - type: string - description: Reference to the external ip address of the new instance - - internalIp: - type: string - description: Reference to tbe internal ip address of the new instance + - networkInterfaces: + type: array + description: | + A list of network interfaces of the new instance. + items: + type: object + properties: + externalIp: + type: string + description: Reference to the external ip address of the new instance + internalIp: + type: string + description: Reference to tbe internal ip address of the new instance - name: type: string description: A name of the instance resource @@ -180,4 +252,3 @@ documentation: examples: - templates/instance/examples/instance.yaml - diff --git a/dm/templates/instance/tests/integration/instance_1_nic.bats b/dm/templates/instance/tests/integration/instance_1_nic.bats new file mode 100755 index 00000000000..379d0633dbc --- /dev/null +++ b/dm/templates/instance/tests/integration/instance_1_nic.bats @@ -0,0 +1,87 @@ +#!/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 in the deployment name with dashes. + DEPLOYMENT_NAME=${DEPLOYMENT_NAME//_/-} + CONFIG=".${DEPLOYMENT_NAME}.yaml" +fi + +########## HELPER FUNCTIONS ########## + +function create_config() { + echo "Creating ${CONFIG}" + envsubst < "templates/instance/tests/integration/${TEST_NAME}.yaml" > "${CONFIG}" +} + +function delete_config() { + echo "Deleting ${CONFIG}" + rm -f "${CONFIG}" +} + +function setup() { + # Global setup; this is executed once per test file. + if [ ${BATS_TEST_NUMBER} -eq 1 ]; then + create_config + fi + + # Per-test setup steps. +} + +function teardown() { + #Global teardown; this is executed once per test file + if [[ "$BATS_TEST_NUMBER" -eq "${#BATS_TEST_NAMES[@]}" ]]; then + 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 a Compute Instance was created in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute instances list --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + [[ "$output" =~ "test-instance-${RAND}" ]] +} + +@test "Verifying that the Compute Instance was connected to the first custom network in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute instances describe test-instance-${RAND} --zone "us-central1-a" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + [[ "$output" =~ "test-network-0-${RAND}" ]] +} + +@test "Verifying that the Compute Instance has the canIpForward property set in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute instances describe test-instance-${RAND} --zone "us-central1-a" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + [[ "$output" =~ "canIpForward: true" ]] +} + +@test "Deleting deployment" { + gcloud deployment-manager deployments delete "${DEPLOYMENT_NAME}" -q \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + run gcloud compute instances list --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ ! "$output" =~ "test-instance-${RAND}" ]] +} diff --git a/dm/templates/instance/tests/integration/instance_1_nic.yaml b/dm/templates/instance/tests/integration/instance_1_nic.yaml new file mode 100644 index 00000000000..ffb660a3566 --- /dev/null +++ b/dm/templates/instance/tests/integration/instance_1_nic.yaml @@ -0,0 +1,36 @@ +# Test of the Instance template. +# +# Variables: +# RAND: A random string used by the testing suite. + +imports: + - path: templates/instance/instance.py + name: instance.py + +resources: + - name: test-instance-${RAND} + type: instance.py + properties: + zone: us-central1-a + diskImage: projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts + diskSizeGb: 100 + machineType: f1-micro + diskType: pd-ssd + canIpForward: true + networks: + - name: $(ref.test-network-0-${RAND}.selfLink) + subnetwork: $(ref.test-subnetwork-0-${RAND}.selfLink) + metadata: + items: + - key: startup-script + value: sudo apt-get update && sudo apt-get install -y nginx + - name: test-network-0-${RAND} + type: compute.v1.network + properties: + autoCreateSubnetworks: false + - name: test-subnetwork-0-${RAND} + type: compute.v1.subnetwork + properties: + network: $(ref.test-network-0-${RAND}.selfLink) + ipCidrRange: 10.0.1.0/24 + region: us-central1 diff --git a/dm/templates/instance/tests/integration/instance_2_nics.bats b/dm/templates/instance/tests/integration/instance_2_nics.bats new file mode 100755 index 00000000000..ae17e82a193 --- /dev/null +++ b/dm/templates/instance/tests/integration/instance_2_nics.bats @@ -0,0 +1,94 @@ +#!/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 in the deployment name with dashes. + DEPLOYMENT_NAME=${DEPLOYMENT_NAME//_/-} + CONFIG=".${DEPLOYMENT_NAME}.yaml" +fi + +########## HELPER FUNCTIONS ########## + +function create_config() { + echo "Creating ${CONFIG}" + envsubst < "templates/instance/tests/integration/${TEST_NAME}.yaml" > "${CONFIG}" +} + +function delete_config() { + echo "Deleting ${CONFIG}" + rm -f "${CONFIG}" +} + +function setup() { + # Global setup; this is executed once per test file. + if [ ${BATS_TEST_NUMBER} -eq 1 ]; then + create_config + fi + + # Per-test setup steps. +} + +function teardown() { + #Global teardown; this is executed once per test file + if [[ "$BATS_TEST_NUMBER" -eq "${#BATS_TEST_NAMES[@]}" ]]; then + 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 a Compute Instance was created in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute instances list --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + [[ "$output" =~ "test-instance-${RAND}" ]] +} + +@test "Verifying that the Compute Instance was connected to the first custom network in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute instances describe test-instance-${RAND} --zone "us-central1-a" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + [[ "$output" =~ "test-network-0-${RAND}" ]] +} + +@test "Verifying that the Compute Instance was connected to the second custom network in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute instances describe test-instance-${RAND} --zone "us-central1-a" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + [[ "$output" =~ "test-network-1-${RAND}" ]] +} + +@test "Verifying that the Compute Instance has the canIpForward property set in deployment ${DEPLOYMENT_NAME}" { + run gcloud compute instances describe test-instance-${RAND} --zone "us-central1-a" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + [[ "$output" =~ "canIpForward: true" ]] +} + +@test "Deleting deployment" { + gcloud deployment-manager deployments delete "${DEPLOYMENT_NAME}" -q \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + + run gcloud compute instances list --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ ! "$output" =~ "test-instance-${RAND}" ]] +} diff --git a/dm/templates/instance/tests/integration/instance_2_nics.yaml b/dm/templates/instance/tests/integration/instance_2_nics.yaml new file mode 100644 index 00000000000..ffbc4a88cd8 --- /dev/null +++ b/dm/templates/instance/tests/integration/instance_2_nics.yaml @@ -0,0 +1,48 @@ +# Test of the Instance template. +# +# Variables: +# RAND: A random string used by the testing suite. + +imports: + - path: templates/instance/instance.py + name: instance.py + +resources: + - name: test-instance-${RAND} + type: instance.py + properties: + zone: us-central1-a + diskImage: projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts + diskSizeGb: 100 + machineType: f1-micro + diskType: pd-ssd + canIpForward: true + networks: + - name: $(ref.test-network-0-${RAND}.selfLink) + subnetwork: $(ref.test-subnetwork-0-${RAND}.selfLink) + - name: $(ref.test-network-1-${RAND}.selfLink) + subnetwork: $(ref.test-subnetwork-1-${RAND}.selfLink) + metadata: + items: + - key: startup-script + value: sudo apt-get update && sudo apt-get install -y nginx + - name: test-network-0-${RAND} + type: compute.v1.network + properties: + autoCreateSubnetworks: false + - name: test-network-1-${RAND} + type: compute.v1.network + properties: + autoCreateSubnetworks: false + - name: test-subnetwork-0-${RAND} + type: compute.v1.subnetwork + properties: + network: $(ref.test-network-0-${RAND}.selfLink) + ipCidrRange: 10.0.1.0/24 + region: us-central1 + - name: test-subnetwork-1-${RAND} + type: compute.v1.subnetwork + properties: + network: $(ref.test-network-1-${RAND}.selfLink) + ipCidrRange: 10.0.2.0/24 + region: us-central1 diff --git a/dm/templates/instance/tests/integration/instance_template/instance_template.bats b/dm/templates/instance/tests/integration/instance_template/instance_template.bats new file mode 100755 index 00000000000..cc22489cb40 --- /dev/null +++ b/dm/templates/instance/tests/integration/instance_template/instance_template.bats @@ -0,0 +1,123 @@ +#!/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 in the deployment name with dashes. + DEPLOYMENT_NAME=${DEPLOYMENT_NAME//_/-} + CONFIG=".${DEPLOYMENT_NAME}.yaml" + export IMAGE="projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts" +fi + +########## HELPER FUNCTIONS ########## + +function create_config() { + envsubst < "templates/instance_template/tests/integration/${TEST_NAME}.yaml" > "${CONFIG}" +} + +function delete_config() { + rm -f "${CONFIG}" +} + +function setup() { + # Global setup; executed once per test file. + if [ ${BATS_TEST_NUMBER} -eq 1 ]; then + 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 + rm -f "${RANDOM_FILE}" + delete_config + fi + + # Per-test teardown steps. +} + + +@test "Creating deployment ${DEPLOYMENT_NAME} from ${CONFIG}" { + run gcloud deployment-manager deployments create "${DEPLOYMENT_NAME}" \ + --config "${CONFIG}" --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] +} + +@test "Verifying instance template disk properties" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.disks[0].initializeParams)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "diskType: pd-ssd" ]] + [[ "$output" =~ "sourceImage: ${IMAGE}" ]] + [[ "$output" =~ "diskSizeGb: '50'" ]] +} + +@test "Verifying instance spec properties" { + run gcloud compute instance-templates describe it-${RAND} \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "machineType: f1-micro" ]] + [[ "$output" =~ "description: Instance description" ]] + [[ "$output" =~ "canIpForward: true" ]] +} + +@test "Verifying instance template properties" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "value(name, description, properties.labels)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "Template description" ]] + [[ "$output" =~ "it-${RAND}" ]] + [[ "$output" =~ "name=wrench" ]] +} + +@test "Verifying instance template network tags" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.tags)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "ftp" ]] + [[ "$output" =~ "https" ]] +} + +@test "Verifying instance template metadata" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.metadata)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "key: createdBy" ]] + [[ "$output" =~ "value: unitTest" ]] +} + +@test "Verifying instance template network properties" { + NET="https://www.googleapis.com/compute/v1/projects/${CLOUD_FOUNDATION_PROJECT_ID}/global/networks/test-network-${RAND}" + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.networkInterfaces[0])" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "name: External NAT" ]] + [[ "$output" =~ "type: ONE_TO_ONE_NAT" ]] + [[ "$output" =~ "network: ${NET}" ]] +} + +@test "Deleting deployment" { + run gcloud deployment-manager deployments delete "${DEPLOYMENT_NAME}" -q \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] +} diff --git a/dm/templates/instance/tests/integration/instance_template/instance_template.yaml b/dm/templates/instance/tests/integration/instance_template/instance_template.yaml new file mode 100644 index 00000000000..847fbce2716 --- /dev/null +++ b/dm/templates/instance/tests/integration/instance_template/instance_template.yaml @@ -0,0 +1,37 @@ +# Test of the Instance Template template. +# +# Variables: +# RAND: a random string used by the testing suite +# IMAGE: a URL to the base disk image provided by the testing suite + +imports: + - path: templates/instance_template/instance_template.py + name: instance_template.py + +resources: + - name: instance-template-${RAND} + type: instance_template.py + properties: + name: it-${RAND} + instanceDescription: Instance description + templateDescription: Template description + network: $(ref.test-network-${RAND}.selfLink) + diskImage: ${IMAGE} + machineType: f1-micro + canIpForward: true + diskType: pd-ssd + diskSizeGb: 50 + tags: + items: + - ftp + - https + metadata: + items: + - key: createdBy + value: unitTest + labels: + name: wrench + - name: test-network-${RAND} + type: compute.v1.network + properties: + autoCreateSubnetworks: true diff --git a/dm/templates/instance_template/examples/instance_template.yaml b/dm/templates/instance_template/examples/instance_template.yaml index f3ee0613c5f..72bc1291ab8 100644 --- a/dm/templates/instance_template/examples/instance_template.yaml +++ b/dm/templates/instance_template/examples/instance_template.yaml @@ -11,7 +11,8 @@ resources: type: instance_template.py properties: diskImage: projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts - network: default + networks: + - default machineType: f1-micro tags: items: diff --git a/dm/templates/instance_template/instance_template.py b/dm/templates/instance_template/instance_template.py index 5349aadb910..9e1802c062a 100644 --- a/dm/templates/instance_template/instance_template.py +++ b/dm/templates/instance_template/instance_template.py @@ -43,38 +43,46 @@ def create_boot_disk(properties): return boot_disk -def get_network(properties): - """ Gets configuration that connects an instance to an existing network - and assigns to it an ephemeral public IP. +def get_network_interfaces(properties): + """ Get the configuration that connects the instance to an existing network + and assigns to it an ephemeral public IP if specified. """ - - network_name = properties.get('network') - is_self_link = '/' in network_name or '.' in network_name - - if is_self_link: - network_url = network_name - else: - network_url = 'global/networks/{}'.format(network_name) - - network_interfaces = { - 'network': network_url - } - - if properties['hasExternalIp']: - access_configs = { - 'name': 'External NAT', - 'type': 'ONE_TO_ONE_NAT' + network_interfaces = [] + + networks = properties.get('networks', [{ + "name": properties.get('network'), + "hasExternalIp": properties.get('hasExternalIp'), + "natIP": properties.get('natIP'), + "subnetwork": properties.get('subnetwork'), + "networkIP": properties.get('networkIP'), + }]) + + for network in networks: + if not '.' in network['name'] and not '/' in network['name']: + network_name = 'global/networks/{}'.format(network['name']) + else: + network_name = network['name'] + + network_interface = { + 'network': network_name, } - if 'natIP' in properties: - access_configs['natIP'] = properties['natIP'] + if network['hasExternalIp']: + access_configs = { + 'name': 'External NAT', + 'type': 'ONE_TO_ONE_NAT' + } + + if network.get('natIP'): + access_configs['natIP'] = network['natIP'] - network_interfaces['accessConfigs'] = [access_configs] + network_interface['accessConfigs'] = [access_configs] - netif_optional_props = ['subnetwork', 'networkIP'] - for prop in netif_optional_props: - if prop in properties: - network_interfaces[prop] = properties[prop] + netif_optional_props = ['subnetwork', 'networkIP'] + for prop in netif_optional_props: + if network.get(prop): + network_interface[prop] = network[prop] + network_interfaces.append(network_interface) return network_interfaces @@ -86,7 +94,7 @@ def generate_config(context): name = properties.get('name', context.env['name']) machine_type = properties['machineType'] boot_disk = create_boot_disk(properties) - network = get_network(properties) + network_interfaces = get_network_interfaces(context.properties) instance_template = { 'name': name, 'type': 'compute.v1.instanceTemplate', @@ -96,7 +104,7 @@ def generate_config(context): { 'machineType': machine_type, 'disks': [boot_disk], - 'networkInterfaces': [network] + 'networkInterfaces': network_interfaces } } } diff --git a/dm/templates/instance_template/instance_template.py.schema b/dm/templates/instance_template/instance_template.py.schema index 1c5c62d30c5..2094b19d4dd 100644 --- a/dm/templates/instance_template/instance_template.py.schema +++ b/dm/templates/instance_template/instance_template.py.schema @@ -20,7 +20,69 @@ info: required: - diskImage - - network + +oneOf: + - allOf: + - required: + - networks + - properties: + networks: + minItems: 1 + - not: + required: + - network + - not: + required: + - natIP + - not: + required: + - subnetwork + - not: + required: + - networkIP + - allOf: + - required: + - network + - not: + required: + - networks + +additionalProperties: false + +definitions: + hasExternalIp: + type: boolean + default: true + description: | + Defines wether the instance will use an external IP from a shared + ephemeral IP address pool. If this is set to false, the instance + will not have an external IP. + natIP: + type: string + description: | + An external IP address associated with this instance. Specify an unused + static external IP address available to the project or leave this field + undefined to use an IP from a shared ephemeral IP address pool. If you + specify a static external IP address, it must live in the same region + as the zone of the instance. + If hasExternalIp is false this field is ignored. + subnetwork: + type: string + description: | + The URL of the Subnetwork resource for this instance. If the network + resource is in legacy mode, do not provide this property. If the network + is in auto subnet mode, providing the subnetwork is optional. If the + network is in custom subnet mode, then this field should be specified. + If you specify this property, you can specify the subnetwork as a full + or partial URL. For example, the following are all valid URLs: + - https://www.googleapis.com/compute/v1/projects/project/regions/region/subnetworks/subnetwork + - regions/region/subnetworks/subnetwork + networkIP: + type: string + description: | + An IPv4 internal network address to assign to the instance for this + network interface. If not specified by the user, an unused internal IP + is assigned by the system. properties: name: @@ -34,44 +96,38 @@ properties: description: | The description of the instance resource the instance template will create (optional). - network: - type: string - description: | - The URL or name of the network where the instance is placed; - e.g., 'my-custom-network' or 'global/networks/default'. hasExternalIp: - type: boolean - default: true - description: | - Defines wether the instance will use an external IP from a shared - ephemeral IP address pool. If this is set to false, the instance - will not have an external IP. + $ref: '#/definitions/hasExternalIp' natIP: - type: string - description: | - An external IP address associated with this instance. Specify an unused - static external IP address available to the project or leave this field - undefined to use an IP from a shared ephemeral IP address pool. If you - specify a static external IP address, it must live in the same region - as the zone of the instance. - If hasExternalIp is false this field is ignored. + $ref: '#/definitions/natIP' subnetwork: - type: string - description: | - The URL of the Subnetwork resource for this instance. If the network - resource is in legacy mode, do not provide this property. If the network - is in auto subnet mode, providing the subnetwork is optional. If the - network is in custom subnet mode, then this field should be specified. - If you specify this property, you can specify the subnetwork as a full - or partial URL. For example, the following are all valid URLs: - - https://www.googleapis.com/compute/v1/projects/project/regions/region/subnetworks/subnetwork - - regions/region/subnetworks/subnetwork + $ref: '#/definitions/subnetwork' networkIP: - type: string + $ref: '#/definitions/networkIP' + networks: + type: array description: | - An IPv4 internal network address to assign to the instance for this - network interface. If not specified by the user, an unused internal IP - is assigned by the system. + Networks the instance will be connected to; + e.g., 'my-custom-network' or 'default'. + items: + type: object + additionalProperties: false + required: + - name + properties: + name: + type: string + description: | + Name of the network the instance will be connected to; + e.g., 'my-custom-network' or 'default'. + hasExternalIp: + $ref: '#/definitions/hasExternalIp' + natIP: + $ref: '#/definitions/natIP' + subnetwork: + $ref: '#/definitions/subnetwork' + networkIP: + $ref: '#/definitions/networkIP' machineType: type: string default: n1-standard-1 diff --git a/dm/templates/instance_template/tests/integration/instance_template_networks.bats b/dm/templates/instance_template/tests/integration/instance_template_networks.bats new file mode 100755 index 00000000000..97c3a9f210a --- /dev/null +++ b/dm/templates/instance_template/tests/integration/instance_template_networks.bats @@ -0,0 +1,134 @@ +#!/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 in the deployment name with dashes. + DEPLOYMENT_NAME=${DEPLOYMENT_NAME//_/-} + CONFIG=".${DEPLOYMENT_NAME}.yaml" + export IMAGE="projects/ubuntu-os-cloud/global/images/family/ubuntu-1804-lts" +fi + +########## HELPER FUNCTIONS ########## + +function create_config() { + envsubst < "templates/instance_template/tests/integration/${TEST_NAME}.yaml" > "${CONFIG}" +} + +function delete_config() { + rm -f "${CONFIG}" +} + +function setup() { + # Global setup; executed once per test file. + if [ ${BATS_TEST_NUMBER} -eq 1 ]; then + 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 + rm -f "${RANDOM_FILE}" + delete_config + fi + + # Per-test teardown steps. +} + + +@test "Creating deployment ${DEPLOYMENT_NAME} from ${CONFIG}" { + run gcloud deployment-manager deployments create "${DEPLOYMENT_NAME}" \ + --config "${CONFIG}" --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] +} + +@test "Verifying instance template disk properties" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.disks[0].initializeParams)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "diskType: pd-ssd" ]] + [[ "$output" =~ "sourceImage: ${IMAGE}" ]] + [[ "$output" =~ "diskSizeGb: '50'" ]] +} + +@test "Verifying instance spec properties" { + run gcloud compute instance-templates describe it-${RAND} \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "machineType: f1-micro" ]] + [[ "$output" =~ "description: Instance description" ]] + [[ "$output" =~ "canIpForward: true" ]] +} + +@test "Verifying instance template properties" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "value(name, description, properties.labels)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "Template description" ]] + [[ "$output" =~ "it-${RAND}" ]] + [[ "$output" =~ "name=wrench" ]] +} + +@test "Verifying instance template network tags" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.tags)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "ftp" ]] + [[ "$output" =~ "https" ]] +} + +@test "Verifying instance template metadata" { + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.metadata)" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "key: createdBy" ]] + [[ "$output" =~ "value: unitTest" ]] +} + +@test "Verifying instance template first network properties" { + NET="https://www.googleapis.com/compute/v1/projects/${CLOUD_FOUNDATION_PROJECT_ID}/global/networks/test-network-0-${RAND}" + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.networkInterfaces[0])" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "name: External NAT" ]] + [[ "$output" =~ "type: ONE_TO_ONE_NAT" ]] + [[ "$output" =~ "network: ${NET}" ]] +} + +@test "Verifying instance template second network properties" { + NET="https://www.googleapis.com/compute/v1/projects/${CLOUD_FOUNDATION_PROJECT_ID}/global/networks/test-network-1-${RAND}" + run gcloud compute instance-templates describe it-${RAND} \ + --format "yaml(properties.networkInterfaces[1])" \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] + [[ "$output" =~ "name: External NAT" ]] + [[ "$output" =~ "type: ONE_TO_ONE_NAT" ]] + [[ "$output" =~ "network: ${NET}" ]] +} + +@test "Deleting deployment" { + run gcloud deployment-manager deployments delete "${DEPLOYMENT_NAME}" -q \ + --project "${CLOUD_FOUNDATION_PROJECT_ID}" + [[ "$status" -eq 0 ]] +} diff --git a/dm/templates/instance_template/tests/integration/instance_template_networks.yaml b/dm/templates/instance_template/tests/integration/instance_template_networks.yaml new file mode 100644 index 00000000000..d753f9a84f0 --- /dev/null +++ b/dm/templates/instance_template/tests/integration/instance_template_networks.yaml @@ -0,0 +1,57 @@ +# Test of the Instance Template template. +# +# Variables: +# RAND: a random string used by the testing suite +# IMAGE: a URL to the base disk image provided by the testing suite + +imports: + - path: templates/instance_template/instance_template.py + name: instance_template.py + +resources: + - name: instance-template-${RAND} + type: instance_template.py + properties: + name: it-${RAND} + instanceDescription: Instance description + templateDescription: Template description + networks: + - name: $(ref.test-network-0-${RAND}.selfLink) + subnetwork: $(ref.test-subnetwork-0-${RAND}.selfLink) + - name: $(ref.test-network-1-${RAND}.selfLink) + subnetwork: $(ref.test-subnetwork-1-${RAND}.selfLink) + diskImage: ${IMAGE} + machineType: f1-micro + canIpForward: true + diskType: pd-ssd + diskSizeGb: 50 + tags: + items: + - ftp + - https + metadata: + items: + - key: createdBy + value: unitTest + labels: + name: wrench + - name: test-network-0-${RAND} + type: compute.v1.network + properties: + autoCreateSubnetworks: false + - name: test-network-1-${RAND} + type: compute.v1.network + properties: + autoCreateSubnetworks: false + - name: test-subnetwork-0-${RAND} + type: compute.v1.subnetwork + properties: + network: $(ref.test-network-0-${RAND}.selfLink) + ipCidrRange: 10.0.1.0/24 + region: us-central1 + - name: test-subnetwork-1-${RAND} + type: compute.v1.subnetwork + properties: + network: $(ref.test-network-1-${RAND}.selfLink) + ipCidrRange: 10.0.2.0/24 + region: us-central1