templates/vm_instance.py (313 lines of code) (raw):
# Copyright 2015 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Creates an Instance VM with common defaults."""
# pylint: disable=g-import-not-at-top
import copy
import common
import default
# Properties for this component
BOOTDISKTYPE = default.BOOTDISK
BOOTDISKSIZE = default.BOOTDISKSIZE
CAN_IP_FWD = default.CAN_IP_FWD
DEVIMAGE = 'devImage'
DISK = default.DISK
DISKTYPE = default.DISKTYPE
DISK_RESOURCES = default.DISK_RESOURCES
ENDPOINT_NAME = default.ENDPOINT_NAME
GUEST_ACCELERATORS = default.GUEST_ACCELERATORS
INSTANCE_NAME = default.INSTANCE_NAME
MACHINETYPE = default.MACHINETYPE
METADATA = default.METADATA
NETWORK = default.NETWORK
SUBNETWORK = default.SUBNETWORK
NO_SCOPE = default.NO_SCOPE
PROJECT = default.PROJECT
PROVIDE_BOOT = default.PROVIDE_BOOT
SERVICE_ACCOUNTS = default.SERVICE_ACCOUNTS
SRCIMAGE = default.SRCIMAGE
TAGS = default.TAGS
ZONE = default.ZONE
AUTODELETE_BOOTDISK = 'bootDiskAutodelete'
STATIC_IP = 'staticIP'
NAT_IP = 'natIP'
HAS_EXTERNAL_IP = 'hasExternalIP'
# Defaults used for modules that imports this one
DEFAULT_DISKTYPE = 'pd-standard'
DEFAULT_IP_FWD = False
DEFAULT_MACHINETYPE = 'n1-standard-1'
DEFAULT_NETWORK = 'default'
DEFAULT_PROVIDE_BOOT = True
DEFAULT_BOOTDISKSIZE = 10
DEFAULT_AUTODELETE_BOOTDISK = True
DEFAULT_STATIC_IP = False
DEFAULT_HAS_EXTERNAL_IP = True
DEFAULT_DATADISKSIZE = 500
DEFAULT_ZONE = 'us-central1-f'
DEFAULT_PERSISTENT = 'PERSISTENT'
DEFAULT_SERVICE_ACCOUNT = [{
'email': 'default',
'scopes': [
'https://www.googleapis.com/auth/cloud.useraccounts.readonly',
'https://www.googleapis.com/auth/devstorage.read_only',
'https://www.googleapis.com/auth/logging.write',
'https://www.googleapis.com/auth/monitoring.write',
]
}]
# Set Metadata Value
ATTACHED_DISKS = 'ATTACHED_DISKS'
# Used for SSD special treatment
SCRATCH = 'SCRATCH'
# Blank image used when sourceImage property is not provided.
BLANK_IMAGE = 'empty10gb'
def MakeVMName(context):
"""Generates the VM name."""
name = context.env['name']
prop = context.properties
named = INSTANCE_NAME in prop
return prop[INSTANCE_NAME] if named else common.AutoName(name,
default.INSTANCE)
def GenerateComputeVM(context, create_disks_separately=True):
"""Generates one VM instance resource.
Args:
context: Template context dictionary.
create_disks_separately: When true (default), all new disk resources are
created as separate resources. This is legacy behaviour from when multiple
disks creation was not allowed in the disks property.
Returns:
dictionary representing instance resource.
"""
prop = context.properties
boot_disk_type = prop.setdefault(BOOTDISKTYPE, DEFAULT_DISKTYPE)
prop[default.DISKTYPE] = boot_disk_type
can_ip_fwd = prop.setdefault(CAN_IP_FWD, DEFAULT_IP_FWD)
disks = prop.setdefault(default.DISKS, list())
local_ssd = prop.setdefault(default.LOCAL_SSD, 0)
if disks:
if create_disks_separately:
# Legacy alternative from when multiple disks on creation were not allowed
new_disks = prop.setdefault(default.DISK_RESOURCES, list())
SetDiskProperties(context, disks)
disks, prop[DISK_RESOURCES] = GenerateDisks(context, disks, new_disks)
else:
# All new disks (except local ssd) must provide a sourceImage or existing
# source. Add blank source image if non provided.
SetDiskProperties(context, disks, add_blank_src_img=True)
machine_type = prop.setdefault(MACHINETYPE, DEFAULT_MACHINETYPE)
metadata = prop.setdefault(METADATA, dict())
network = prop.setdefault(NETWORK, DEFAULT_NETWORK)
vm_name = MakeVMName(context)
provide_boot = prop.setdefault(PROVIDE_BOOT, DEFAULT_PROVIDE_BOOT)
tags = prop.setdefault(TAGS, dict([('items', [])]))
zone = prop.setdefault(ZONE, DEFAULT_ZONE)
has_external_ip = prop.get(HAS_EXTERNAL_IP, DEFAULT_HAS_EXTERNAL_IP)
static_ip = prop.get(STATIC_IP, DEFAULT_STATIC_IP)
nat_ip = prop.get(NAT_IP, None)
if provide_boot:
dev_mode = DEVIMAGE in prop and prop[DEVIMAGE]
src_image = common.MakeC2DImageLink(prop[SRCIMAGE], dev_mode)
boot_name = common.AutoName(context.env['name'], default.DISK, 'boot')
disk_size = prop.get(BOOTDISKSIZE, DEFAULT_BOOTDISKSIZE)
disk_type = common.MakeLocalComputeLink(context, DISKTYPE)
autodelete = prop.get(AUTODELETE_BOOTDISK, DEFAULT_AUTODELETE_BOOTDISK)
disks = PrependBootDisk(disks, boot_name, disk_type, disk_size, src_image,
autodelete)
if local_ssd:
disks = AppendLocalSSDDisks(context, disks, local_ssd)
machine_type = common.MakeLocalComputeLink(context, default.MACHINETYPE)
network = common.MakeGlobalComputeLink(context, default.NETWORK)
subnetwork = ''
if default.SUBNETWORK in prop:
subnetwork = common.MakeSubnetworkComputeLink(context, default.SUBNETWORK)
# To be consistent with Dev console and gcloud, service accounts need to be
# explicitly disabled
remove_scopes = prop[NO_SCOPE] if NO_SCOPE in prop else False
if remove_scopes and SERVICE_ACCOUNTS in prop:
prop.pop(SERVICE_ACCOUNTS)
else: # Make sure there is a default service account
prop.setdefault(SERVICE_ACCOUNTS, copy.deepcopy(DEFAULT_SERVICE_ACCOUNT))
resource = []
access_configs = []
if has_external_ip:
access_config = {'name': default.EXTERNAL, 'type': default.ONE_NAT}
access_configs.append(access_config)
if static_ip and nat_ip:
raise common.Error(
'staticIP=True and natIP cannot be specified at the same time')
if static_ip:
address_resource, nat_ip = MakeStaticAddress(vm_name, zone)
resource.append(address_resource)
if nat_ip:
access_config['natIP'] = nat_ip
else:
if static_ip:
raise common.Error('staticIP cannot be True when hasExternalIP is False')
if nat_ip:
raise common.Error(
'natIP must not be specified when hasExternalIP is False')
network_interfaces = []
if subnetwork:
network_interfaces.insert(0, {
'network': network,
'subnetwork': subnetwork,
'accessConfigs': access_configs
})
else:
network_interfaces.insert(0, {
'network': network,
'accessConfigs': access_configs
})
resource.insert(0, {
'name': vm_name,
'type': default.INSTANCE,
'properties': {
'zone': zone,
'machineType': machine_type,
'canIpForward': can_ip_fwd,
'disks': disks,
'networkInterfaces': network_interfaces,
'tags': tags,
'metadata': metadata,
}
})
# Pass through any additional properties to the VM
if SERVICE_ACCOUNTS in prop:
resource[0]['properties'].update({SERVICE_ACCOUNTS: prop[SERVICE_ACCOUNTS]})
if GUEST_ACCELERATORS in prop:
for accelerators in prop[GUEST_ACCELERATORS]:
accelerators['acceleratorType'] = common.MakeAcceleratorTypeLink(
context, accelerators['acceleratorType'])
resource[0]['properties'].update(
{GUEST_ACCELERATORS: prop[GUEST_ACCELERATORS]})
# GPUs cannot be attached to live migratable instances. See:
# https://cloud.google.com/compute/docs/gpus/#restrictions
resource[0]['properties'].update(
{'scheduling': {'onHostMaintenance': 'terminate'}})
return resource
def MakeStaticAddress(vm_name, zone):
"""Creates a static IP address resource; returns it and the natIP."""
address_name = vm_name + '-address'
address_resource = {
'name': address_name,
'type': default.ADDRESS,
'properties': {
'name': address_name,
'region': common.ZoneToRegion(zone),
},
}
return (address_resource, '$(ref.%s.address)' % address_name)
def PrependBootDisk(disk_list, name, disk_type, disk_size, src_image,
autodelete):
"""Appends the boot disk."""
# Request boot disk creation (mark for autodelete)
boot_disk = [{
'autoDelete': autodelete,
'boot': True,
'deviceName': name,
'initializeParams': {
'diskType': disk_type,
'diskSizeGb': disk_size,
'sourceImage': src_image
},
'type': DEFAULT_PERSISTENT,
}]
return boot_disk + disk_list
def AppendLocalSSDDisks(context, disk_list, num_of_local_ssd):
"""Apends local ssds."""
project = context.env[default.PROJECT]
prop = context.properties
zone = prop.setdefault(ZONE, DEFAULT_ZONE)
local_ssd_disks = []
for i in range(0, num_of_local_ssd):
local_ssd_disks.append({
'deviceName': 'local-ssd-%s' % i,
'type': SCRATCH,
'interface': 'SCSI',
'mode': 'READ_WRITE',
'autoDelete': True,
'initializeParams': {'diskType': common.LocalComputeLink(
project, zone, 'diskTypes', 'local-ssd')}
})
return disk_list + local_ssd_disks
def SetDiskProperties(context, disks, add_blank_src_img=False):
"""Set properties on each disk to required format.
Sets default values, and moves properties passed directly into
initializeParams where required.
Args:
context: Template context dictionary.
disks: List of disks to set properties on.
add_blank_src_img: When true, link to blank source image is added for new
disks where a source image is not specified.
"""
project = context.env[default.PROJECT]
zone = context.properties.setdefault(ZONE, DEFAULT_ZONE)
for disk in disks:
disk.setdefault(default.AUTO_DELETE, True)
disk.setdefault('boot', False)
disk.setdefault(default.TYPE, DEFAULT_PERSISTENT)
# If disk already exists, no properties to change.
if default.DISK_SOURCE in disk:
continue
else:
disk_init = disk.setdefault(default.INITIALIZEP, dict())
if disk[default.TYPE] == SCRATCH:
disk_init.setdefault(DISKTYPE, 'local-ssd')
else:
# In the Instance API reference, size and type are within this property
if disk_init:
disk_init.setdefault(default.DISK_SIZE, DEFAULT_DATADISKSIZE)
disk_init.setdefault(default.DISKTYPE, DEFAULT_DISKTYPE)
# You can also simply pass the size and type properties directly
else:
disk_init[default.DISK_SIZE] = disk.pop(default.DISK_SIZE,
DEFAULT_DATADISKSIZE)
disk_init[default.DISKTYPE] = disk.pop(default.DISKTYPE,
DEFAULT_DISKTYPE)
# If disk name was given as a direct property, move to initializeParams
if default.DISK_NAME in disk:
disk_init[default.DISK_NAME] = disk.pop(default.DISK_NAME)
# Add link to a blank source image where non-specified
if add_blank_src_img and default.SRCIMAGE not in disk_init:
disk_init[default.SRCIMAGE] = common.MakeC2DImageLink(BLANK_IMAGE)
# Change disk type names into URLs
disk_init[default.DISKTYPE] = common.LocalComputeLink(
project, zone, 'diskTypes', disk_init[default.DISKTYPE])
def GenerateDisks(context, disk_list, new_disks):
"""Generates as many disks as passed in the disk_list."""
prop = context.properties
zone = prop.setdefault(ZONE, DEFAULT_ZONE)
sourced_disks = []
disk_names = []
for disk in disk_list:
if default.DISK_SOURCE in disk or disk[default.TYPE] == SCRATCH:
# These disks do not need to be created as separate resources
sourced_disks.append(disk)
else:
# Extract disk parameters and create as separate resource
disk_init = disk[default.INITIALIZEP]
if default.DEVICE_NAME in disk:
d_name = disk[default.DEVICE_NAME]
elif default.DISK_NAME in disk_init:
d_name = disk_init[default.DISK_NAME]
else:
raise common.Error('deviceName or diskName is needed for each disk in '
'this module implemention of multiple disks per vm.')
new_disks.append({
'name': d_name,
'type': default.DISK,
'properties': {
'type': disk_init[default.DISKTYPE],
'sizeGb': disk_init[default.DISK_SIZE],
'zone': zone
}
})
disk_names.append(d_name)
source = common.Ref(d_name)
sourced_disks.append({
'deviceName': d_name,
'autoDelete': disk[default.AUTO_DELETE],
'boot': False,
'source': source,
'type': disk[default.TYPE],
})
items = prop[METADATA].setdefault('items', list())
items.append({'key': ATTACHED_DISKS, 'value': ','.join(disk_names)})
return sourced_disks, new_disks
def AddServiceEndpointIfNeeded(context):
"""If the endpoint property is present, it will add a service endpoint."""
prop = context.properties
if ENDPOINT_NAME not in prop:
return []
network = common.MakeGlobalComputeLink(context, default.NETWORK)
reference = '$(ref.' + MakeVMName(context) + '.name)'
address = common.MakeFQHN(context, reference)
name = prop[ENDPOINT_NAME]
resource = [
{
'name': name,
'type': default.ENDPOINT,
'properties': {
'addresses': [
{'address': address}
],
'dnsIntegration': {
'networks': [network]
}
}
}
]
return resource
def GenerateResourceList(context, **kwargs):
"""Returns list of resources generated by this module."""
resources = GenerateComputeVM(context, **kwargs)
resources += common.AddDiskResourcesIfNeeded(context)
resources += AddServiceEndpointIfNeeded(context)
return resources
def GenerateOutputList(context, resource_list):
"""Returns list of outputs generated by this module."""
vm_res = resource_list[0]
outputs = [{
'name': 'internalIP',
'value': '$(ref.%s.networkInterfaces[0].networkIP)' % vm_res['name'],
}]
has_external_ip = context.properties.get(HAS_EXTERNAL_IP,
DEFAULT_HAS_EXTERNAL_IP)
if has_external_ip:
outputs.append({
'name': 'ip',
'value': ('$(ref.%s.networkInterfaces[0].accessConfigs[0].natIP)' %
vm_res['name']),
})
return outputs
@common.FormatErrorsDec
def GenerateConfig(context):
"""Generates YAML resource configuration."""
resource_list = GenerateResourceList(context)
output_list = GenerateOutputList(context, resource_list)
return common.MakeResource(resource_list, output_list)