From cf9c45a40e68d8422290731676aff892afe63bb2 Mon Sep 17 00:00:00 2001 From: Randall Burt Date: Thu, 13 Jun 2013 12:58:29 -0500 Subject: [PATCH] Implement attribute schema for resources Similar to properties, adds attribute_schema and attributes members to Resources in order to facilitate document generation and template provider stubs for resources. Change-Id: Ie858fc71a91078e14af552d8cafe0f2448f5d2b8 Implements: blueprint attributes-schema --- heat/engine/resources/autoscaling.py | 10 ++++--- heat/engine/resources/dbinstance.py | 19 ++++++++------ heat/engine/resources/eip.py | 13 +++++----- heat/engine/resources/instance.py | 32 +++++++++++++---------- heat/engine/resources/loadbalancer.py | 33 +++++++++++++----------- heat/engine/resources/quantum/net.py | 16 ++++++------ heat/engine/resources/quantum/port.py | 21 +++++++++------ heat/engine/resources/quantum/quantum.py | 10 +++++++ heat/engine/resources/quantum/router.py | 16 ++++++------ heat/engine/resources/quantum/subnet.py | 24 +++++++++++------ heat/engine/resources/s3.py | 18 ++++++------- heat/engine/resources/stack.py | 11 +------- heat/engine/stack_resource.py | 19 +++++++++++++- heat/tests/test_eip.py | 3 ++- heat/tests/test_loadbalancer.py | 2 +- heat/tests/test_parser.py | 2 +- heat/tests/test_s3.py | 3 ++- 17 files changed, 150 insertions(+), 102 deletions(-) diff --git a/heat/engine/resources/autoscaling.py b/heat/engine/resources/autoscaling.py index 509c412f..fe75db2e 100644 --- a/heat/engine/resources/autoscaling.py +++ b/heat/engine/resources/autoscaling.py @@ -73,6 +73,10 @@ class InstanceGroup(resource.Resource): } update_allowed_keys = ('Properties',) update_allowed_properties = ('Size',) + attributes_schema = { + "InstanceList": ("A comma-delimited list of server ip addresses. " + "(Heat extension)") + } def handle_create(self): return self.resize(int(self.properties['Size']), raise_on_error=True) @@ -220,14 +224,14 @@ class InstanceGroup(resource.Resource): def FnGetRefId(self): return unicode(self.name) - def FnGetAtt(self, key): + def _resolve_attribute(self, name): ''' heat extension: "InstanceList" returns comma delimited list of server ip addresses. ''' - if key == 'InstanceList': + if name == 'InstanceList': if self.resource_id is None: - return '' + return None name_list = sorted(self.resource_id.split(',')) inst_list = [] for name in name_list: diff --git a/heat/engine/resources/dbinstance.py b/heat/engine/resources/dbinstance.py index 0bfd4d88..ea7ff596 100644 --- a/heat/engine/resources/dbinstance.py +++ b/heat/engine/resources/dbinstance.py @@ -13,9 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from heat.common import exception -from heat.engine import stack_resource from heat.common import template_format +from heat.engine import stack_resource from heat.openstack.common import log as logging logger = logging.getLogger(__name__) @@ -193,6 +192,13 @@ class DBInstance(stack_resource.StackResource): 'Implemented': False}, } + # We only support a couple of the attributes right now + attributes_schema = { + "Endpoint.Address": "Connection endpoint for the database.", + "Endpoint.Port": ("The port number on which the database accepts " + "connections.") + } + def _params(self): params = { 'KeyName': {'Ref': 'KeyName'}, @@ -219,20 +225,17 @@ class DBInstance(stack_resource.StackResource): def handle_delete(self): self.delete_nested() - def FnGetAtt(self, key): + def _resolve_attribute(self, name): ''' We don't really support any of these yet. ''' - if key == 'Endpoint.Address': + if name == 'Endpoint.Address': if self.nested() and 'DatabaseInstance' in self.nested().resources: return self.nested().resources['DatabaseInstance']._ipaddress() else: return '0.0.0.0' - elif key == 'Endpoint.Port': + elif name == 'Endpoint.Port': return self.properties['Port'] - else: - raise exception.InvalidTemplateAttribute(resource=self.name, - key=key) def resource_mapping(): diff --git a/heat/engine/resources/eip.py b/heat/engine/resources/eip.py index dc568565..94a43ef0 100644 --- a/heat/engine/resources/eip.py +++ b/heat/engine/resources/eip.py @@ -14,7 +14,6 @@ # under the License. from heat.engine import clients -from heat.common import exception from heat.engine import resource from heat.openstack.common import log as logging @@ -26,6 +25,11 @@ class ElasticIp(resource.Resource): properties_schema = {'Domain': {'Type': 'String', 'Implemented': False}, 'InstanceId': {'Type': 'String'}} + attributes_schema = { + "AllocationId": ("ID that AWS assigns to represent the allocation of" + "the address for use with Amazon VPC. Returned only" + " for VPC elastic IP addresses.") + } def __init__(self, name, json_snippet, stack): super(ElasticIp, self).__init__(name, json_snippet, stack) @@ -69,12 +73,9 @@ class ElasticIp(resource.Resource): def FnGetRefId(self): return unicode(self._ipaddress()) - def FnGetAtt(self, key): - if key == 'AllocationId': + def _resolve_attribute(self, name): + if name == 'AllocationId': return unicode(self.resource_id) - else: - raise exception.InvalidTemplateAttribute(resource=self.name, - key=key) class ElasticIpAssociation(resource.Resource): diff --git a/heat/engine/resources/instance.py b/heat/engine/resources/instance.py index 4c492ded..1d8fbca0 100644 --- a/heat/engine/resources/instance.py +++ b/heat/engine/resources/instance.py @@ -109,6 +109,18 @@ class Instance(resource.Resource): 'UserData': {'Type': 'String'}, 'Volumes': {'Type': 'List'}} + attributes_schema = {'AvailabilityZone': ('The Availability Zone where the' + ' specified instance is ' + 'launched.'), + 'PrivateDnsName': ('Private DNS name of the specified' + ' instance.'), + 'PublicDnsName': ('Public DNS name of the specified ' + 'instance.'), + 'PrivateIp': ('Private IP address of the specified ' + 'instance.'), + 'PublicIp': ('Public IP address of the specified ' + 'instance.')} + # template keys supported for handle_update, note trailing comma # is required for a single item to get a tuple not a string update_allowed_keys = ('Metadata',) @@ -153,24 +165,16 @@ class Instance(resource.Resource): return self.ipaddress or '0.0.0.0' - def FnGetAtt(self, key): + def _resolve_attribute(self, name): res = None - if key == 'AvailabilityZone': + if name == 'AvailabilityZone': res = self.properties['AvailabilityZone'] - elif key == 'PublicIp': - res = self._ipaddress() - elif key == 'PrivateIp': - res = self._ipaddress() - elif key == 'PublicDnsName': + elif name in ['PublicIp', 'PrivateIp', 'PublicDnsName', + 'PrivateDnsName']: res = self._ipaddress() - elif key == 'PrivateDnsName': - res = self._ipaddress() - else: - raise exception.InvalidTemplateAttribute(resource=self.name, - key=key) - logger.info('%s.GetAtt(%s) == %s' % (self.name, key, res)) - return unicode(res) + logger.info('%s._resolve_attribute(%s) == %s' % (self.name, name, res)) + return unicode(res) if res else None def _build_userdata(self, userdata): if not self.mime_string: diff --git a/heat/engine/resources/loadbalancer.py b/heat/engine/resources/loadbalancer.py index e8a05e82..a13308f2 100644 --- a/heat/engine/resources/loadbalancer.py +++ b/heat/engine/resources/loadbalancer.py @@ -13,9 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. -from heat.engine import clients -from heat.common import exception from heat.common import template_format +from heat.engine import clients from heat.engine import stack_resource from heat.openstack.common import log as logging @@ -241,6 +240,18 @@ class LoadBalancer(stack_resource.StackResource): 'Subnets': {'Type': 'List', 'Implemented': False} } + attributes_schema = { + "CanonicalHostedZoneName": ("The name of the hosted zone that is " + "associated with the LoadBalancer."), + "CanonicalHostedZoneNameID": ("The ID of the hosted zone name that is " + "associated with the LoadBalancer."), + "DNSName": "The DNS name for the LoadBalancer.", + "SourceSecurityGroup.GroupName": ("The security group that you can use" + " as part of your inbound rules for " + "your LoadBalancer's back-end " + "instances."), + "SourceSecurityGroup.OwnerAlias": "Owner of the source security group." + } update_allowed_keys = ('Properties',) update_allowed_properties = ('Instances',) @@ -371,23 +382,15 @@ class LoadBalancer(stack_resource.StackResource): def FnGetRefId(self): return unicode(self.name) - def FnGetAtt(self, key): + def _resolve_attribute(self, name): ''' We don't really support any of these yet. ''' - allow = ('CanonicalHostedZoneName', - 'CanonicalHostedZoneNameID', - 'DNSName', - 'SourceSecurityGroupName', - 'SourceSecurityGroupOwnerAlias') - - if key not in allow: - raise exception.InvalidTemplateAttribute(resource=self.name, - key=key) - - if key == 'DNSName': + if name == 'DNSName': return self.get_output('PublicIp') - else: + elif name in self.attributes_schema: + # Not sure if we should return anything for the other attribs + # since they aren't really supported in any meaningful way return '' diff --git a/heat/engine/resources/quantum/net.py b/heat/engine/resources/quantum/net.py index 4a476b9b..b5a5a580 100644 --- a/heat/engine/resources/quantum/net.py +++ b/heat/engine/resources/quantum/net.py @@ -29,6 +29,14 @@ class Net(quantum.QuantumResource): 'Default': {}}, 'admin_state_up': {'Default': True, 'Type': 'Boolean'}} + attributes_schema = { + "id": "the unique identifier for this network", + "status": "the status of the network", + "name": "the name of the network", + "subnets": "subnets of this network", + "admin_state_up": "the administrative status of the network", + "tenant_id": "the tenant owning this network" + } def handle_create(self): props = self.prepare_properties( @@ -53,14 +61,6 @@ class Net(quantum.QuantumResource): if ex.status_code != 404: raise ex - def FnGetAtt(self, key): - try: - attributes = self._show_resource() - except QuantumClientException as ex: - logger.warn("failed to fetch resource attributes: %s" % str(ex)) - return None - return self.handle_get_attributes(self.name, key, attributes) - def resource_mapping(): if clients.quantumclient is None: diff --git a/heat/engine/resources/quantum/port.py b/heat/engine/resources/quantum/port.py index 901be105..de72238d 100644 --- a/heat/engine/resources/quantum/port.py +++ b/heat/engine/resources/quantum/port.py @@ -42,6 +42,19 @@ class Port(quantum.QuantumResource): 'mac_address': {'Type': 'String'}, 'device_id': {'Type': 'String'}, 'security_groups': {'Type': 'List'}} + attributes_schema = { + "admin_state_up": "the administrative state of this port", + "device_id": "unique identifier for the device", + "device_owner": "name of the network owning the port", + "fixed_ips": "fixed ip addresses", + "id": "the unique identifier for the port", + "mac_address": "mac address of the port", + "name": "friendly name of the port", + "network_id": "unique identifier for the network owning the port", + "security_groups": "a list of security groups for the port", + "status": "the status of the port", + "tenant_id": "tenant owning the port" + } def handle_create(self): props = self.prepare_properties( @@ -66,14 +79,6 @@ class Port(quantum.QuantumResource): if ex.status_code != 404: raise ex - def FnGetAtt(self, key): - try: - attributes = self._show_resource() - except QuantumClientException as ex: - logger.warn("failed to fetch resource attributes: %s" % str(ex)) - return None - return self.handle_get_attributes(self.name, key, attributes) - def resource_mapping(): if clients.quantumclient is None: diff --git a/heat/engine/resources/quantum/quantum.py b/heat/engine/resources/quantum/quantum.py index 6d60f232..34bf0afc 100644 --- a/heat/engine/resources/quantum/quantum.py +++ b/heat/engine/resources/quantum/quantum.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from quantumclient.common.exceptions import QuantumClientException + from heat.common import exception from heat.engine import resource @@ -92,5 +94,13 @@ class QuantumResource(resource.Resource): ('quantum reported unexpected', attributes['name'], attributes['status'])) + def _resolve_attribute(self, name): + try: + attributes = self._show_resource() + except QuantumClientException as ex: + logger.warn("failed to fetch resource attributes: %s" % str(ex)) + return None + return self.handle_get_attributes(self.name, name, attributes) + def FnGetRefId(self): return unicode(self.resource_id) diff --git a/heat/engine/resources/quantum/router.py b/heat/engine/resources/quantum/router.py index d7c0498e..898a7a84 100644 --- a/heat/engine/resources/quantum/router.py +++ b/heat/engine/resources/quantum/router.py @@ -30,6 +30,14 @@ class Router(quantum.QuantumResource): 'Default': {}}, 'admin_state_up': {'Type': 'Boolean', 'Default': True}} + attributes_schema = { + "status": "the status of the router", + "external_gateway_info": "gateway network for the router", + "name": "friendly name of the router", + "admin_state_up": "administrative state of the router", + "tenant_id": "tenant owning the router", + "id": "unique identifier for the router" + } def handle_create(self): props = self.prepare_properties( @@ -54,14 +62,6 @@ class Router(quantum.QuantumResource): if ex.status_code != 404: raise ex - def FnGetAtt(self, key): - try: - attributes = self._show_resource() - except QuantumClientException as ex: - logger.warn("failed to fetch resource attributes: %s" % str(ex)) - return None - return self.handle_get_attributes(self.name, key, attributes) - class RouterInterface(quantum.QuantumResource): properties_schema = {'router_id': {'Type': 'String', diff --git a/heat/engine/resources/quantum/subnet.py b/heat/engine/resources/quantum/subnet.py index 24a1221e..a75df953 100644 --- a/heat/engine/resources/quantum/subnet.py +++ b/heat/engine/resources/quantum/subnet.py @@ -47,6 +47,20 @@ class Subnet(quantum.QuantumResource): 'Type': 'Map', 'Schema': allocation_schema }}} + attributes_schema = { + "name": "friendly name of the subnet", + "network_id": "parent network of the subnet", + "tenant_id": "tenant owning the subnet", + "allocation_pools": "ip allocation pools and their ranges", + "gateway_ip": "ip of the subnet's gateway", + "ip_version": "ip version for the subnet", + "cidr": "CIDR block notation for this subnet", + "id": "unique identifier for this subnet", + # dns_nameservers isn't in the api docs; is it right? + "dns_nameservers": "list of dns nameservers", + "enable_dhcp": ("'true' if DHCP is enabled for this subnet; 'false'" + "otherwise") + } def handle_create(self): props = self.prepare_properties( @@ -63,14 +77,8 @@ class Subnet(quantum.QuantumResource): if ex.status_code != 404: raise ex - def FnGetAtt(self, key): - try: - attributes = self.quantum().show_subnet( - self.resource_id)['subnet'] - except QuantumClientException as ex: - logger.warn("failed to fetch resource attributes: %s" % str(ex)) - return None - return self.handle_get_attributes(self.name, key, attributes) + def _show_resource(self): + return self.quantum().show_subnet(self.resource_id)['subnet'] def resource_mapping(): diff --git a/heat/engine/resources/s3.py b/heat/engine/resources/s3.py index 751f21fd..f82c4953 100644 --- a/heat/engine/resources/s3.py +++ b/heat/engine/resources/s3.py @@ -15,10 +15,9 @@ from urlparse import urlparse -from heat.common import exception +from heat.engine import clients from heat.engine import resource from heat.openstack.common import log as logging -from heat.engine import clients logger = logging.getLogger(__name__) @@ -36,6 +35,10 @@ class S3Bucket(resource.Resource): 'BucketOwnerFullControl']}, 'WebsiteConfiguration': {'Type': 'Map', 'Schema': website_schema}} + attributes_schema = { + "DomainName": "The DNS name of the specified bucket.", + "WebsiteURL": "The website endpoint for the specified bucket." + } def validate(self): ''' @@ -89,17 +92,14 @@ class S3Bucket(resource.Resource): def FnGetRefId(self): return unicode(self.resource_id) - def FnGetAtt(self, key): - url, token_id = self.swift().get_auth() + def _resolve_attribute(self, name): + url = self.swift().get_auth()[0] parsed = list(urlparse(url)) - if key == 'DomainName': + if name == 'DomainName': return parsed[1].split(':')[0] - elif key == 'WebsiteURL': + elif name == 'WebsiteURL': return '%s://%s%s/%s' % (parsed[0], parsed[1], parsed[2], self.resource_id) - else: - raise exception.InvalidTemplateAttribute(resource=self.name, - key=key) def resource_mapping(): diff --git a/heat/engine/resources/stack.py b/heat/engine/resources/stack.py index 4366f412..ed0cf8f5 100644 --- a/heat/engine/resources/stack.py +++ b/heat/engine/resources/stack.py @@ -13,10 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from heat.common import exception -from heat.engine import stack_resource from heat.common import template_format from heat.common import urlfetch +from heat.engine import stack_resource from heat.openstack.common import log as logging @@ -52,14 +51,6 @@ class NestedStack(stack_resource.StackResource): def FnGetRefId(self): return self.nested().identifier().arn() - def FnGetAtt(self, key): - if not key.startswith('Outputs.'): - raise exception.InvalidTemplateAttribute( - resource=self.name, key=key) - - prefix, dot, op = key.partition('.') - return unicode(self.get_output(op)) - def resource_mapping(): return { diff --git a/heat/engine/stack_resource.py b/heat/engine/stack_resource.py index 1bab2627..71f6a0cb 100644 --- a/heat/engine/stack_resource.py +++ b/heat/engine/stack_resource.py @@ -14,9 +14,10 @@ # under the License. from heat.common import exception +from heat.engine import attributes from heat.engine import environment -from heat.engine import resource from heat.engine import parser +from heat.engine import resource from heat.engine import scheduler from heat.openstack.common import log as logging @@ -32,8 +33,18 @@ class StackResource(resource.Resource): def __init__(self, name, json_snippet, stack): super(StackResource, self).__init__(name, json_snippet, stack) + self._outputs_to_attribs(json_snippet) self._nested = None + def _outputs_to_attribs(self, json_snippet): + if not self.attributes and 'Outputs' in json_snippet: + self.attributes_schema = ( + attributes.Attributes + .schema_from_outputs(json_snippet.get('Outputs'))) + self.attributes = attributes.Attributes(self.name, + self.attributes_schema, + self._resolve_attribute) + def nested(self): ''' Return a Stack object representing the nested (child) stack. @@ -53,6 +64,7 @@ class StackResource(resource.Resource): Handle the creation of the nested stack from a given JSON template. ''' template = parser.Template(child_template) + self._outputs_to_attribs(child_template) # Note we disable rollback for nested stacks, since they # should be rolled back by the parent stack on failure @@ -105,3 +117,8 @@ class StackResource(resource.Resource): resource=self.name, key=op) return stack.output(op) + + def _resolve_attribute(self, name): + if name.startswith('Outputs.'): + name = name.partition('.')[-1] + return unicode(self.get_output(name)) diff --git a/heat/tests/test_eip.py b/heat/tests/test_eip.py index 70a606d9..7715877b 100644 --- a/heat/tests/test_eip.py +++ b/heat/tests/test_eip.py @@ -13,6 +13,7 @@ # under the License. +from heat.common import exception from heat.common import template_format from heat.engine.resources import eip from heat.engine import resource @@ -116,7 +117,7 @@ class EIPTest(HeatTestCase): self.assertRaises(resource.UpdateReplace, rsrc.handle_update, {}, {}, {}) - self.assertRaises(eip.exception.InvalidTemplateAttribute, + self.assertRaises(exception.InvalidTemplateAttribute, rsrc.FnGetAtt, 'Foo') finally: diff --git a/heat/tests/test_loadbalancer.py b/heat/tests/test_loadbalancer.py index 87286782..aeab875a 100644 --- a/heat/tests/test_loadbalancer.py +++ b/heat/tests/test_loadbalancer.py @@ -167,7 +167,7 @@ class LoadBalancerTest(HeatTestCase): rsrc.handle_update(rsrc.json_snippet, {}, {'Instances': id_list}) self.assertEqual('4.5.6.7', rsrc.FnGetAtt('DNSName')) - self.assertEqual('', rsrc.FnGetAtt('SourceSecurityGroupName')) + self.assertEqual('', rsrc.FnGetAtt('SourceSecurityGroup.GroupName')) try: rsrc.FnGetAtt('Foo') diff --git a/heat/tests/test_parser.py b/heat/tests/test_parser.py index d0637077..46eca8dc 100644 --- a/heat/tests/test_parser.py +++ b/heat/tests/test_parser.py @@ -1300,7 +1300,7 @@ class StackTest(HeatTestCase): self.assertTrue('AResource' in self.stack) rsrc = self.stack['AResource'] rsrc.resource_id_set('aaaa') - self.assertEqual('AResource', rsrc.FnGetAtt('foo')) + self.assertEqual('AResource', rsrc.FnGetAtt('Foo')) for action, status in ( (rsrc.CREATE, rsrc.IN_PROGRESS), diff --git a/heat/tests/test_s3.py b/heat/tests/test_s3.py index 8270cc8f..40f5e764 100644 --- a/heat/tests/test_s3.py +++ b/heat/tests/test_s3.py @@ -15,6 +15,7 @@ from testtools import skipIf +from heat.common import exception from heat.common import template_format from heat.openstack.common.importutils import try_import from heat.engine.resources import s3 @@ -100,7 +101,7 @@ class s3Test(HeatTestCase): try: rsrc.FnGetAtt('Foo') raise Exception('Expected InvalidTemplateAttribute') - except s3.exception.InvalidTemplateAttribute: + except exception.InvalidTemplateAttribute: pass self.assertRaises(resource.UpdateReplace, -- 2.45.2