Convert Server to new Schema format

blueprint property-schema-conversion

Change-Id: I2da66244760f782e774b332b07b08e526ce5a7ba
This commit is contained in:
Zane Bitter 2014-01-09 19:16:04 -05:00
parent 36c0382d64
commit f574a01e9a

View File

@ -21,6 +21,8 @@ from heat.common import exception
from heat.engine import clients from heat.engine import clients
from heat.engine import scheduler from heat.engine import scheduler
from heat.engine.resources import nova_utils from heat.engine.resources import nova_utils
from heat.engine import constraints
from heat.engine import properties
from heat.engine import resource from heat.engine import resource
from heat.openstack.common.gettextutils import _ from heat.openstack.common.gettextutils import _
from heat.openstack.common import log as logging from heat.openstack.common import log as logging
@ -31,154 +33,202 @@ logger = logging.getLogger(__name__)
class Server(resource.Resource): class Server(resource.Resource):
block_mapping_schema = { PROPERTIES = (
'device_name': { NAME, IMAGE, BLOCK_DEVICE_MAPPING, FLAVOR,
'Type': 'String', FLAVOR_UPDATE_POLICY, IMAGE_UPDATE_POLICY, KEY_NAME,
'Required': True, ADMIN_USER, AVAILABILITY_ZONE, SECURITY_GROUPS, NETWORKS,
'Description': _('A device name where the volume will be ' SCHEDULER_HINTS, METADATA, USER_DATA_FORMAT, USER_DATA,
'attached in the system at /dev/device_name. ' RESERVATION_ID, CONFIG_DRIVE, DISK_CONFIG,
'This value is typically vda.')}, ) = (
'volume_id': { 'name', 'image', 'block_device_mapping', 'flavor',
'Type': 'String', 'flavor_update_policy', 'image_update_policy', 'key_name',
'Description': _('The ID of the volume to boot from. Only one of ' 'admin_user', 'availability_zone', 'security_groups', 'networks',
'volume_id or snapshot_id should be provided.')}, 'scheduler_hints', 'metadata', 'user_data_format', 'user_data',
'snapshot_id': { 'reservation_id', 'config_drive', 'diskConfig',
'Type': 'String', )
'Description': _('The ID of the snapshot to create a volume '
'from.')},
'volume_size': {
'Type': 'String',
'Description': _('The size of the volume, in GB. It is safe to '
'leave this blank and have the Compute service '
'infer the size.')},
'delete_on_termination': {
'Type': 'Boolean',
'Description': _('Indicate whether the volume should be deleted '
'when the server is terminated.')}
}
networks_schema = { _BLOCK_DEVICE_MAPPING_KEYS = (
'uuid': { BLOCK_DEVICE_MAPPING_DEVICE_NAME, BLOCK_DEVICE_MAPPING_VOLUME_ID,
'Type': 'String', BLOCK_DEVICE_MAPPING_SNAPSHOT_ID,
'Description': _('DEPRECATED! ID of network ' BLOCK_DEVICE_MAPPING_VOLUME_SIZE,
'to create a port on.')}, BLOCK_DEVICE_MAPPING_DELETE_ON_TERM,
'network': { ) = (
'Type': 'String', 'device_name', 'volume_id',
'Description': _('Name or ID of network to create a port on.')}, 'snapshot_id',
'fixed_ip': { 'volume_size',
'Type': 'String', 'delete_on_termination',
'Description': _('Fixed IP address to specify for the port ' )
'created on the requested network.')},
'port': { _NETWORK_KEYS = (
'Type': 'String', NETWORK_UUID, NETWORK_ID, NETWORK_FIXED_IP, NETWORK_PORT,
'Description': _('ID of an existing port to associate with ' ) = (
'this server.')}, 'uuid', 'network', 'fixed_ip', 'port',
} )
properties_schema = { properties_schema = {
'name': { NAME: properties.Schema(
'Type': 'String', properties.Schema.STRING,
'Description': _('Server name.')}, _('Server name.')
'image': { ),
'Type': 'String', IMAGE: properties.Schema(
'Description': _('The ID or name of the image to boot with.'), properties.Schema.STRING,
'UpdateAllowed': True}, _('The ID or name of the image to boot with.'),
'block_device_mapping': { update_allowed=True
'Type': 'List', ),
'Description': _('Block device mappings for this server.'), BLOCK_DEVICE_MAPPING: properties.Schema(
'Schema': { properties.Schema.LIST,
'Type': 'Map', _('Block device mappings for this server.'),
'Schema': block_mapping_schema schema=properties.Schema(
} properties.Schema.MAP,
}, schema={
'flavor': { BLOCK_DEVICE_MAPPING_DEVICE_NAME: properties.Schema(
'Type': 'String', properties.Schema.STRING,
'Description': _('The ID or name of the flavor to boot onto.'), _('A device name where the volume will be '
'Required': True, 'attached in the system at /dev/device_name. '
'UpdateAllowed': True}, 'This value is typically vda.'),
'flavor_update_policy': { required=True
'Type': 'String', ),
'Description': _('Policy on how to apply a flavor update; either ' BLOCK_DEVICE_MAPPING_VOLUME_ID: properties.Schema(
'by requesting a server resize or by replacing ' properties.Schema.STRING,
'the entire server.'), _('The ID of the volume to boot from. Only one '
'Default': 'RESIZE', 'of volume_id or snapshot_id should be '
'AllowedValues': ['RESIZE', 'REPLACE'], 'provided.')
'UpdateAllowed': True}, ),
'image_update_policy': { BLOCK_DEVICE_MAPPING_SNAPSHOT_ID: properties.Schema(
'Type': 'String', properties.Schema.STRING,
'Default': 'REPLACE', _('The ID of the snapshot to create a volume '
'Description': _('Policy on how to apply an image-id update; ' 'from.')
'either by requesting a server rebuild or by ' ),
'replacing the entire server'), BLOCK_DEVICE_MAPPING_VOLUME_SIZE: properties.Schema(
'AllowedValues': ['REBUILD', 'REPLACE', properties.Schema.STRING,
'REBUILD_PRESERVE_EPHEMERAL'], _('The size of the volume, in GB. It is safe to '
'UpdateAllowed': True}, 'leave this blank and have the Compute service '
'key_name': { 'infer the size.')
'Type': 'String', ),
'Description': _('Name of keypair to inject into the server.')}, BLOCK_DEVICE_MAPPING_DELETE_ON_TERM: properties.Schema(
'admin_user': { properties.Schema.BOOLEAN,
'Type': 'String', _('Indicate whether the volume should be deleted '
'Default': cfg.CONF.instance_user, 'when the server is terminated.')
'Description': _('Name of the administrative user to use ' ),
'on the server.')}, },
'availability_zone': { )
'Type': 'String', ),
'Description': _('Name of the availability zone for server ' FLAVOR: properties.Schema(
'placement.')}, properties.Schema.STRING,
'security_groups': { _('The ID or name of the flavor to boot onto.'),
'Type': 'List', required=True,
'Description': _('List of security group names or IDs.'), update_allowed=True
'Default': []}, ),
'networks': { FLAVOR_UPDATE_POLICY: properties.Schema(
'Type': 'List', properties.Schema.STRING,
'Description': _('An ordered list of nics to be ' _('Policy on how to apply a flavor update; either by requesting '
'added to this server, with information about ' 'a server resize or by replacing the entire server.'),
'connected networks, fixed ips, port etc.'), default='RESIZE',
'Schema': { constraints=[
'Type': 'Map', constraints.AllowedValues(['RESIZE', 'REPLACE']),
'Schema': networks_schema ],
} update_allowed=True
}, ),
'scheduler_hints': { IMAGE_UPDATE_POLICY: properties.Schema(
'Type': 'Map', properties.Schema.STRING,
'Description': _('Arbitrary key-value pairs specified by the ' _('Policy on how to apply an image-id update; either by '
'client to help boot a server.')}, 'requesting a server rebuild or by replacing the entire server'),
'metadata': { default='REPLACE',
'Type': 'Map', constraints=[
'UpdateAllowed': True, constraints.AllowedValues(['REBUILD', 'REPLACE',
'Description': _('Arbitrary key/value metadata to store for this ' 'REBUILD_PRESERVE_EPHEMERAL']),
'server. Both keys and values must be 255 ' ],
'characters or less.')}, update_allowed=True
'user_data_format': { ),
'Type': 'String', KEY_NAME: properties.Schema(
'Default': 'HEAT_CFNTOOLS', properties.Schema.STRING,
'Description': _('How the user_data should be formatted for the ' _('Name of keypair to inject into the server.')
'server. For HEAT_CFNTOOLS, the user_data is ' ),
'bundled as part of the heat-cfntools ' ADMIN_USER: properties.Schema(
'cloud-init boot configuration data. For RAW, ' properties.Schema.STRING,
'the user_data is passed to Nova unmodified.'), _('Name of the administrative user to use on the server.'),
'AllowedValues': ['HEAT_CFNTOOLS', 'RAW']}, default=cfg.CONF.instance_user
'user_data': { ),
'Type': 'String', AVAILABILITY_ZONE: properties.Schema(
'Description': _('User data script to be executed by ' properties.Schema.STRING,
'cloud-init.'), _('Name of the availability zone for server placement.')
'Default': ""}, ),
'reservation_id': { SECURITY_GROUPS: properties.Schema(
'Type': 'String', properties.Schema.LIST,
'Description': _('A UUID for the set of servers being requested.') _('List of security group names or IDs.'),
}, default=[]
'config_drive': { ),
'Type': 'String', NETWORKS: properties.Schema(
'Description': _('value for config drive either boolean, or ' properties.Schema.LIST,
'volume-id.') _('An ordered list of nics to be added to this server, with '
}, 'information about connected networks, fixed ips, port etc.'),
# diskConfig translates to API attribute OS-DCF:diskConfig schema=properties.Schema(
# hence the camel case instead of underscore to separate the words properties.Schema.MAP,
'diskConfig': { schema={
'Type': 'String', NETWORK_UUID: properties.Schema(
'Description': _('Control how the disk is partitioned when the ' properties.Schema.STRING,
'server is created.'), _('DEPRECATED! ID of network to create a port on.'),
'AllowedValues': ['AUTO', 'MANUAL']} ),
NETWORK_ID: properties.Schema(
properties.Schema.STRING,
_('Name or ID of network to create a port on.')
),
NETWORK_FIXED_IP: properties.Schema(
properties.Schema.STRING,
_('Fixed IP address to specify for the port '
'created on the requested network.')
),
NETWORK_PORT: properties.Schema(
properties.Schema.STRING,
_('ID of an existing port to associate with this '
'server.')
),
},
)
),
SCHEDULER_HINTS: properties.Schema(
properties.Schema.MAP,
_('Arbitrary key-value pairs specified by the client to help '
'boot a server.')
),
METADATA: properties.Schema(
properties.Schema.MAP,
_('Arbitrary key/value metadata to store for this server. Both '
'keys and values must be 255 characters or less.'),
update_allowed=True
),
USER_DATA_FORMAT: properties.Schema(
properties.Schema.STRING,
_('How the user_data should be formatted for the server. For '
'HEAT_CFNTOOLS, the user_data is bundled as part of the '
'heat-cfntools cloud-init boot configuration data. For RAW, '
'the user_data is passed to Nova unmodified.'),
default='HEAT_CFNTOOLS',
constraints=[
constraints.AllowedValues(['HEAT_CFNTOOLS', 'RAW']),
]
),
USER_DATA: properties.Schema(
properties.Schema.STRING,
_('User data script to be executed by cloud-init.'),
default=''
),
RESERVATION_ID: properties.Schema(
properties.Schema.STRING,
_('A UUID for the set of servers being requested.')
),
CONFIG_DRIVE: properties.Schema(
properties.Schema.STRING,
_('value for config drive either boolean, or volume-id.')
),
DISK_CONFIG: properties.Schema(
properties.Schema.STRING,
_('Control how the disk is partitioned when the server is '
'created.'),
constraints=[
constraints.AllowedValues(['AUTO', 'MANUAL']),
]
),
} }
attributes_schema = { attributes_schema = {
@ -210,48 +260,48 @@ class Server(resource.Resource):
super(Server, self).__init__(name, json_snippet, stack) super(Server, self).__init__(name, json_snippet, stack)
def physical_resource_name(self): def physical_resource_name(self):
name = self.properties.get('name') name = self.properties.get(self.NAME)
if name: if name:
return name return name
return super(Server, self).physical_resource_name() return super(Server, self).physical_resource_name()
def handle_create(self): def handle_create(self):
security_groups = self.properties.get('security_groups') security_groups = self.properties.get(self.SECURITY_GROUPS)
user_data_format = self.properties.get('user_data_format') user_data_format = self.properties.get(self.USER_DATA_FORMAT)
userdata = nova_utils.build_userdata( userdata = nova_utils.build_userdata(
self, self,
self.properties.get('user_data'), self.properties.get(self.USER_DATA),
instance_user=self.properties['admin_user'], instance_user=self.properties[self.ADMIN_USER],
user_data_format=user_data_format) user_data_format=user_data_format)
flavor = self.properties['flavor'] flavor = self.properties[self.FLAVOR]
availability_zone = self.properties['availability_zone'] availability_zone = self.properties[self.AVAILABILITY_ZONE]
key_name = self.properties['key_name'] key_name = self.properties[self.KEY_NAME]
if key_name: if key_name:
# confirm keypair exists # confirm keypair exists
nova_utils.get_keypair(self.nova(), key_name) nova_utils.get_keypair(self.nova(), key_name)
image = self.properties.get('image') image = self.properties.get(self.IMAGE)
if image: if image:
image = nova_utils.get_image_id(self.nova(), image) image = nova_utils.get_image_id(self.nova(), image)
flavor_id = nova_utils.get_flavor_id(self.nova(), flavor) flavor_id = nova_utils.get_flavor_id(self.nova(), flavor)
instance_meta = self.properties.get('metadata') instance_meta = self.properties.get(self.METADATA)
if instance_meta is not None: if instance_meta is not None:
instance_meta = dict((key, str(value)) for (key, value) in instance_meta = dict((key, str(value)) for (key, value) in
instance_meta.items()) instance_meta.items())
scheduler_hints = self.properties.get('scheduler_hints') scheduler_hints = self.properties.get(self.SCHEDULER_HINTS)
nics = self._build_nics(self.properties.get('networks')) nics = self._build_nics(self.properties.get(self.NETWORKS))
block_device_mapping = self._build_block_device_mapping( block_device_mapping = self._build_block_device_mapping(
self.properties.get('block_device_mapping')) self.properties.get(self.BLOCK_DEVICE_MAPPING))
reservation_id = self.properties.get('reservation_id') reservation_id = self.properties.get(self.RESERVATION_ID)
config_drive = self.properties.get('config_drive') config_drive = self.properties.get(self.CONFIG_DRIVE)
disk_config = self.properties.get('diskConfig') disk_config = self.properties.get(self.DISK_CONFIG)
server = None server = None
try: try:
@ -303,26 +353,31 @@ class Server(resource.Resource):
status=server.status)) status=server.status))
raise exc raise exc
@staticmethod @classmethod
def _build_block_device_mapping(bdm): def _build_block_device_mapping(cls, bdm):
if not bdm: if not bdm:
return None return None
bdm_dict = {} bdm_dict = {}
for mapping in bdm: for mapping in bdm:
mapping_parts = [] mapping_parts = []
if mapping.get('snapshot_id'): snapshot_id = mapping.get(cls.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID)
mapping_parts.append(mapping.get('snapshot_id')) if snapshot_id:
mapping_parts.append(snapshot_id)
mapping_parts.append('snap') mapping_parts.append('snap')
else: else:
mapping_parts.append(mapping.get('volume_id')) volume_id = mapping.get(cls.BLOCK_DEVICE_MAPPING_VOLUME_ID)
mapping_parts.append(volume_id)
mapping_parts.append('') mapping_parts.append('')
if (mapping.get('volume_size') or
mapping.get('delete_on_termination')):
mapping_parts.append(mapping.get('volume_size', '0')) volume_size = mapping.get(cls.BLOCK_DEVICE_MAPPING_VOLUME_SIZE)
if mapping.get('delete_on_termination'): delete = mapping.get(cls.BLOCK_DEVICE_MAPPING_DELETE_ON_TERM)
mapping_parts.append(str(mapping.get('delete_on_termination'))) if volume_size or delete:
bdm_dict[mapping.get('device_name')] = ':'.join(mapping_parts) mapping_parts.append(volume_size or '0')
if delete:
mapping_parts.append(str(delete))
device_name = mapping.get(cls.BLOCK_DEVICE_MAPPING_DEVICE_NAME)
bdm_dict[device_name] = ':'.join(mapping_parts)
return bdm_dict return bdm_dict
@ -334,19 +389,19 @@ class Server(resource.Resource):
for net_data in networks: for net_data in networks:
nic_info = {} nic_info = {}
if net_data.get('uuid'): if net_data.get(self.NETWORK_UUID):
nic_info['net-id'] = net_data['uuid'] nic_info['net-id'] = net_data[self.NETWORK_UUID]
label_or_uuid = net_data.get('network') label_or_uuid = net_data.get(self.NETWORK_ID)
if label_or_uuid: if label_or_uuid:
if uuidutils.is_uuid_like(label_or_uuid): if uuidutils.is_uuid_like(label_or_uuid):
nic_info['net-id'] = label_or_uuid nic_info['net-id'] = label_or_uuid
else: else:
network = self.nova().networks.find(label=label_or_uuid) network = self.nova().networks.find(label=label_or_uuid)
nic_info['net-id'] = network.id nic_info['net-id'] = network.id
if net_data.get('fixed_ip'): if net_data.get(self.NETWORK_FIXED_IP):
nic_info['v4-fixed-ip'] = net_data['fixed_ip'] nic_info['v4-fixed-ip'] = net_data[self.NETWORK_FIXED_IP]
if net_data.get('port'): if net_data.get(self.NETWORK_PORT):
nic_info['port-id'] = net_data['port'] nic_info['port-id'] = net_data[self.NETWORK_PORT]
nics.append(nic_info) nics.append(nic_info)
return nics return nics
@ -375,22 +430,22 @@ class Server(resource.Resource):
checkers = [] checkers = []
server = None server = None
if 'metadata' in prop_diff: if self.METADATA in prop_diff:
server = self.nova().servers.get(self.resource_id) server = self.nova().servers.get(self.resource_id)
nova_utils.meta_update(self.nova(), nova_utils.meta_update(self.nova(),
server, server,
prop_diff['metadata']) prop_diff[self.METADATA])
if 'flavor' in prop_diff: if self.FLAVOR in prop_diff:
flavor_update_policy = ( flavor_update_policy = (
prop_diff.get('flavor_update_policy') or prop_diff.get(self.FLAVOR_UPDATE_POLICY) or
self.properties.get('flavor_update_policy')) self.properties.get(self.FLAVOR_UPDATE_POLICY))
if flavor_update_policy == 'REPLACE': if flavor_update_policy == 'REPLACE':
raise resource.UpdateReplace(self.name) raise resource.UpdateReplace(self.name)
flavor = prop_diff['flavor'] flavor = prop_diff[self.FLAVOR]
flavor_id = nova_utils.get_flavor_id(self.nova(), flavor) flavor_id = nova_utils.get_flavor_id(self.nova(), flavor)
if not server: if not server:
server = self.nova().servers.get(self.resource_id) server = self.nova().servers.get(self.resource_id)
@ -398,13 +453,13 @@ class Server(resource.Resource):
flavor_id) flavor_id)
checkers.append(checker) checkers.append(checker)
if 'image' in prop_diff: if self.IMAGE in prop_diff:
image_update_policy = ( image_update_policy = (
prop_diff.get('image_update_policy') or prop_diff.get(self.IMAGE_UPDATE_POLICY) or
self.properties.get('image_update_policy')) self.properties.get(self.IMAGE_UPDATE_POLICY))
if image_update_policy == 'REPLACE': if image_update_policy == 'REPLACE':
raise resource.UpdateReplace(self.name) raise resource.UpdateReplace(self.name)
image = prop_diff['image'] image = prop_diff[self.IMAGE]
image_id = nova_utils.get_image_id(self.nova(), image) image_id = nova_utils.get_image_id(self.nova(), image)
if not server: if not server:
server = self.nova().servers.get(self.resource_id) server = self.nova().servers.get(self.resource_id)
@ -445,28 +500,32 @@ class Server(resource.Resource):
super(Server, self).validate() super(Server, self).validate()
# check validity of key # check validity of key
key_name = self.properties.get('key_name') key_name = self.properties.get(self.KEY_NAME)
if key_name: if key_name:
nova_utils.get_keypair(self.nova(), key_name) nova_utils.get_keypair(self.nova(), key_name)
# either volume_id or snapshot_id needs to be specified, but not both # either volume_id or snapshot_id needs to be specified, but not both
# for block device mapping. # for block device mapping.
bdm = self.properties.get('block_device_mapping') or [] bdm = self.properties.get(self.BLOCK_DEVICE_MAPPING) or []
bootable_vol = False bootable_vol = False
for mapping in bdm: for mapping in bdm:
if mapping['device_name'] == 'vda': device_name = mapping[self.BLOCK_DEVICE_MAPPING_DEVICE_NAME]
bootable_vol = True if device_name == 'vda':
bootable_vol = True
if mapping.get('volume_id') and mapping.get('snapshot_id'): volume_id = mapping.get(self.BLOCK_DEVICE_MAPPING_VOLUME_ID)
raise exception.ResourcePropertyConflict('volume_id', snapshot_id = mapping.get(self.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID)
'snapshot_id') if volume_id and snapshot_id:
if not mapping.get('volume_id') and not mapping.get('snapshot_id'): raise exception.ResourcePropertyConflict(
self.BLOCK_DEVICE_MAPPING_VOLUME_ID,
self.BLOCK_DEVICE_MAPPING_SNAPSHOT_ID)
if not volume_id and not snapshot_id:
msg = _('Either volume_id or snapshot_id must be specified for' msg = _('Either volume_id or snapshot_id must be specified for'
' device mapping %s') % mapping['device_name'] ' device mapping %s') % device_name
raise exception.StackValidationFailed(message=msg) raise exception.StackValidationFailed(message=msg)
# make sure the image exists if specified. # make sure the image exists if specified.
image = self.properties.get('image') image = self.properties.get(self.IMAGE)
if image: if image:
nova_utils.get_image_id(self.nova(), image) nova_utils.get_image_id(self.nova(), image)
elif not image and not bootable_vol: elif not image and not bootable_vol:
@ -475,22 +534,26 @@ class Server(resource.Resource):
raise exception.StackValidationFailed(message=msg) raise exception.StackValidationFailed(message=msg)
# network properties 'uuid' and 'network' shouldn't be used # network properties 'uuid' and 'network' shouldn't be used
# both at once for all networks # both at once for all networks
networks = self.properties.get('networks') or [] networks = self.properties.get(self.NETWORKS) or []
for network in networks: for network in networks:
if network.get('uuid') and network.get('network'): if network.get(self.NETWORK_UUID) and network.get(self.NETWORK_ID):
msg = _('Properties "uuid" and "network" are both set ' msg = _('Properties "%(uuid)s" and "%(id)s" are both set '
'to the network "%(network)s" for the server ' 'to the network "%(network)s" for the server '
'"%(server)s". The "uuid" property is deprecated. ' '"%(server)s". The "%(uuid)s" property is deprecated. '
'Use only "network" property.' 'Use only "%(id)s" property.'
'') % dict(network=network['network'], '') % dict(uuid=self.NETWORK_UUID,
id=self.NETWORK_ID,
network=network[self.NETWORK_ID],
server=self.name) server=self.name)
raise exception.StackValidationFailed(message=msg) raise exception.StackValidationFailed(message=msg)
elif network.get('uuid'): elif network.get(self.NETWORK_UUID):
logger.info(_('For the server "%(server)s" the "uuid" ' logger.info(_('For the server "%(server)s" the "%(uuid)s" '
'property is set to network "%(network)s". ' 'property is set to network "%(network)s". '
'"uuid" property is deprecated. Use "network" ' '"%(uuid)s" property is deprecated. Use '
'property instead.' '"%(id)s" property instead.'
'') % dict(network=network['network'], '') % dict(uuid=self.NETWORK_UUID,
id=self.NETWORK_ID,
network=network[self.NETWORK_ID],
server=self.name)) server=self.name))
# verify that the number of metadata entries is not greater # verify that the number of metadata entries is not greater