EC2: Reorganize code for EC2 tags

This change makes the EC2 tags' code more generic, so that
tags for new resources can be added easily.

Progress on:
Blueprint: ec2-volume-and-snapshot-tags
Change-Id: Iba774d55c84dbbef57c56cd331b3ffcd7e41f3ec
This commit is contained in:
Rushi Agrawal 2014-08-03 17:16:35 +05:30
parent f48827dcfa
commit fc05ab545c
4 changed files with 170 additions and 42 deletions

View File

@ -208,6 +208,9 @@ def _format_mappings(properties, result):
result['blockDeviceMapping'] = mappings
_TAGS_SUPPORTED_RESOURCE_TYPES = ['instance']
class CloudController(object):
"""CloudController provides the critical dispatch between
inbound API calls through the endpoint and messages
@ -1785,8 +1788,9 @@ class CloudController(object):
raise exception.InvalidParameterValue(message=msg)
for r in resources:
if ec2utils.resource_type_from_id(context, r) != 'instance':
msg = _('Only instances implemented')
res_type = ec2utils.resource_type_from_id(context, r)
if res_type not in _TAGS_SUPPORTED_RESOURCE_TYPES:
msg = _('Tags not implemented for %s.') % res_type
raise exception.InvalidParameterValue(message=msg)
if not isinstance(tags, (tuple, list, set)):
@ -1808,12 +1812,14 @@ class CloudController(object):
metadata[key] = val
for ec2_id in resources:
instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, ec2_id)
instance = self.compute_api.get(context, instance_uuid,
want_objects=True)
self.compute_api.update_instance_metadata(context,
instance, metadata)
for res_id in resources:
res_type = ec2utils.resource_type_from_id(context, res_id)
if res_type == 'instance':
instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, res_id)
instance = self.compute_api.get(context, instance_uuid,
want_objects=True)
self.compute_api.update_instance_metadata(context,
instance, metadata)
return True
@ -1835,30 +1841,35 @@ class CloudController(object):
raise exception.InvalidParameterValue(message=msg)
for r in resources:
if ec2utils.resource_type_from_id(context, r) != 'instance':
msg = _('Only instances implemented')
res_type = ec2utils.resource_type_from_id(context, r)
if res_type not in _TAGS_SUPPORTED_RESOURCE_TYPES:
msg = _('Tags not implemented for %s.') % res_type
raise exception.InvalidParameterValue(message=msg)
if not isinstance(tags, (tuple, list, set)):
msg = _('Expecting a list of tagSets')
raise exception.InvalidParameterValue(message=msg)
for ec2_id in resources:
instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, ec2_id)
instance = self.compute_api.get(context, instance_uuid,
want_objects=True)
def _ensure_tags_correct(tags):
for tag in tags:
if not isinstance(tag, dict):
msg = _('Expecting tagSet to be key/value pairs')
raise exception.InvalidParameterValue(message=msg)
key = tag.get('key', None)
if key is None:
if tag.get('key') is None:
msg = _('Expecting key to be set')
raise exception.InvalidParameterValue(message=msg)
self.compute_api.delete_instance_metadata(context,
instance, key)
for res_id in resources:
res_type = ec2utils.resource_type_from_id(context, res_id)
if res_type == 'instance':
instance_uuid = ec2utils.ec2_inst_id_to_uuid(context, res_id)
instance = self.compute_api.get(context, instance_uuid,
want_objects=True)
_ensure_tags_correct(tags)
for tag in tags:
self.compute_api.delete_instance_metadata(context,
instance, tag.get('key'))
return True
@ -1870,7 +1881,31 @@ class CloudController(object):
:param context: context under which the method is called
"""
filters = kwargs.get('filter', None)
search_filts = []
valid_resources = set(['customer-gateway', 'dhcp-options', 'image',
'instance', 'internet-gateway', 'network-acl',
'network-interface', 'reserved-instances',
'route-table', 'security-group', 'snapshot',
'spot-instances-request', 'subnet', 'volume',
'vpc', 'vpn-connection', 'vpn-gateway'])
implemented_resources = set(_TAGS_SUPPORTED_RESOURCE_TYPES)
# NOTE(rushiagr): example of the above variable 'filters', for ref:
# [{'name': 'resource-id', 'value': {'1': 'i-00000001'}},
# {'name': 'resource-type', 'value': {'1': 'instance'}},
# {'name': 'value', 'value': {'1': 'production', '2': 'beta'}},
# {'name': 'key', 'value': {'1': 'stack'}}]
# Convert the 'filters' list to a dict for better usability. So for
# example, the above list will be converted to:
# {'resource-id': 'i-00000001',
# 'resource-type': 'instance',
# 'value': ['production', 'beta'],
# 'key': 'stack'}
filter_dict = {}
if filters:
for filter_block in filters:
key_name = filter_block.get('name', None)
@ -1881,32 +1916,103 @@ class CloudController(object):
if not isinstance(val, (tuple, list, set)):
val = (val,)
if key_name:
search_block = {}
if key_name in ('resource_id', 'resource-id'):
search_block['resource_id'] = []
for res_id in val:
search_block['resource_id'].append(
ec2utils.ec2_inst_id_to_uuid(context, res_id))
elif key_name in ['key', 'value']:
search_block[key_name] = \
if key_name in ['key', 'value']:
filter_dict[key_name] = \
[ec2utils.regex_from_ec2_regex(v) for v in val]
elif key_name in ('resource_type', 'resource-type'):
filter_dict['resource_type'] = []
for res_type in val:
if res_type != 'instance':
if (res_type not in implemented_resources or
res_type not in valid_resources):
raise exception.InvalidParameterValue(
message=_('Only instances implemented'))
search_block[key_name] = 'instance'
if len(search_block.keys()) > 0:
search_filts.append(search_block)
message=_('Resource %s not '
'implemented/valid') % res_type)
filter_dict['resource_type'].append(res_type)
elif key_name in ('resource_id', 'resource-id'):
filter_dict['resource_id'] = []
for res_id in val:
filter_dict['resource_id'].append(res_id)
else:
raise exception.InvalidParameterValue(
message=_('Invalid key name: %s') % key_name)
resources_to_query = set()
resource_id_dict = {} # Keys are resource types, values resource IDs
if 'resource_type' not in filter_dict and 'resource_id' in filter_dict:
for res_id in filter_dict['resource_id']:
res_type = ec2utils.resource_type_from_id(context, res_id)
resources_to_query.add(res_type)
if res_type not in resource_id_dict:
resource_id_dict[res_type] = \
[ec2utils.uuid_from_only_resource_id(res_id)]
else:
resource_id_dict[res_type].append(
ec2utils.uuid_from_only_resource_id(res_id))
elif ('resource_type' in filter_dict and
'resource_id' not in filter_dict):
for res_type in filter_dict['resource_type']:
resources_to_query.add(res_type)
elif 'resource_type' in filter_dict and 'resource_id' in filter_dict:
# Say if resource_type has two values 'instance' and 'volume', and
# resource_id has two values 'vol-00000001' and 'ami-00000001', we
# should only check for volumes and not for instances or images.
resources_from_id = set()
resources_from_type = set()
temp_resource_id_dict = {}
for res_id in filter_dict['resource_id']:
res_type = ec2utils.resource_type_from_id(context, res_id)
resources_from_id.add(res_type)
if res_type not in temp_resource_id_dict:
temp_resource_id_dict[res_type] = \
[ec2utils.uuid_from_only_resource_id(res_id)]
else:
temp_resource_id_dict[res_type].append(
ec2utils.uuid_from_only_resource_id(res_id))
for res_type in filter_dict['resource_type']:
resources_from_type.add(res_type)
resources_to_query = resources_from_type.intersection(
resources_from_id)
for res in resources_to_query:
resource_id_dict[res] = temp_resource_id_dict[res]
else:
resources_to_query = implemented_resources
ts = []
for tag in self.compute_api.get_all_instance_metadata(context,
search_filts):
ts.append({
'resource_id': ec2utils.id_to_ec2_inst_id(tag['instance_id']),
'resource_type': 'instance',
'key': tag['key'],
'value': tag['value']
})
if not resources_to_query:
LOG.warning(_LW("Specified resource ID(s) %(ids)s does not "
"belong to specified resource type(s) %(types)s.") %
{'ids': str(filter_dict['resource_id']),
'types': str(filter_dict['resource_type'])})
return {'tagSet': ts}
def _search_filters_for(resource_type):
"""Prepares search filters for the specified resource."""
resource_filter_dict = filter_dict
if resource_type in resource_id_dict:
resource_filter_dict['resource_id'] = \
resource_id_dict[resource_type]
if 'resource_type' in resource_filter_dict:
resource_filter_dict.pop('resource_type')
search_filters = [{k: v} for k, v in
resource_filter_dict.iteritems()]
return search_filters
if 'instance' in resources_to_query:
tags = self.compute_api.get_all_instance_metadata(context,
_search_filters_for('instance'))
for tag in tags:
ts.append({
'resource_id': ec2utils.id_to_ec2_inst_id(
tag['instance_id']),
'resource_type': 'instance',
'key': tag['key'],
'value': tag['value']
})
return {"tagSet": ts}

View File

@ -194,8 +194,24 @@ def id_to_ec2_inst_id(instance_id):
return id_to_ec2_id(instance_id)
def uuid_from_only_resource_id(res_id):
ctxt = context.get_admin_context()
res_type = resource_type_from_id(ctxt, res_id)
if res_type == 'volume':
return ec2_vol_id_to_uuid(res_id)
elif res_type == 'instance':
return ec2_inst_id_to_uuid(ctxt, res_id)
elif res_type == 'snapshot':
return ec2_snap_id_to_uuid(res_id)
else:
raise exception.InvalidEc2Id(ec2_id=res_id)
def ec2_inst_id_to_uuid(context, ec2_id):
""""Convert an instance id to uuid."""
if resource_type_from_id(context, ec2_id) != 'instance':
raise exception.InvalidEc2Id(ec2_id)
int_id = ec2_id_to_id(ec2_id)
return get_instance_uuid_from_int_id(context, int_id)
@ -230,6 +246,9 @@ def ec2_vol_id_to_uuid(ec2_id):
"""Get the corresponding UUID for the given ec2-id."""
ctxt = context.get_admin_context()
if resource_type_from_id(context, ec2_id) != 'volume':
raise exception.InvalidEc2Id(ec2_id)
# NOTE(jgriffith) first strip prefix to get just the numeric
int_id = ec2_id_to_id(ec2_id)
return get_volume_uuid_from_int_id(ctxt, int_id)
@ -334,6 +353,9 @@ def ec2_snap_id_to_uuid(ec2_id):
"""Get the corresponding UUID for the given ec2-id."""
ctxt = context.get_admin_context()
if resource_type_from_id(context, ec2_id) != 'snapshot':
raise exception.InvalidEc2Id(ec2_id)
# NOTE(jgriffith) first strip prefix to get just the numeric
int_id = ec2_id_to_id(ec2_id)
return get_snapshot_uuid_from_int_id(ctxt, int_id)

View File

@ -948,7 +948,7 @@ class CloudTestCase(test.TestCase):
# A filter with even one invalid id should cause an exception to be
# raised
self.assertRaises(exception.InstanceNotFound,
self.assertRaises(exception.InvalidEc2Id,
self.cloud.describe_instances, self.context,
instance_id=[instance_id, '435679'])

View File

@ -83,7 +83,7 @@ class EC2ValidateTestCase(test.TestCase):
self.volume_id_exception_map = [(x,
exception.InvalidInstanceIDMalformed)
for x in self.EC2_MALFORMED_IDS]
self.volume_id_exception_map.extend([(x, exception.VolumeNotFound)
self.volume_id_exception_map.extend([(x, exception.InvalidEc2Id)
for x in self.EC2_VALID__IDS])
def fake_show(meh, context, id, **kwargs):