]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Implement attribute schema for resources
authorRandall Burt <randall.burt@rackspace.com>
Thu, 13 Jun 2013 17:58:29 +0000 (12:58 -0500)
committerRandall Burt <randall.burt@rackspace.com>
Wed, 19 Jun 2013 23:59:54 +0000 (18:59 -0500)
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

17 files changed:
heat/engine/resources/autoscaling.py
heat/engine/resources/dbinstance.py
heat/engine/resources/eip.py
heat/engine/resources/instance.py
heat/engine/resources/loadbalancer.py
heat/engine/resources/quantum/net.py
heat/engine/resources/quantum/port.py
heat/engine/resources/quantum/quantum.py
heat/engine/resources/quantum/router.py
heat/engine/resources/quantum/subnet.py
heat/engine/resources/s3.py
heat/engine/resources/stack.py
heat/engine/stack_resource.py
heat/tests/test_eip.py
heat/tests/test_loadbalancer.py
heat/tests/test_parser.py
heat/tests/test_s3.py

index 509c412fc14e9bac0abfd06b435852bd3b5f290b..fe75db2ef59875adfe92b2d83764846689e858f6 100644 (file)
@@ -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:
index 0bfd4d8876af45c6a9558028caecb30052c5482f..ea7ff5969a2b1e1010dee3e115e2953255ecfaa7 100644 (file)
@@ -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():
index dc5685653ec2ed9c2c2f2fc295575f6436e0f64c..94a43ef0c1d42a92747719a2215498d86a931349 100644 (file)
@@ -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):
index 4c492ded3314e9af7dae0535c3c13b7fe4f65b26..1d8fbca035ed34dd11fc7b874404feda5928814c 100644 (file)
@@ -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:
index e8a05e82f873e43f17ae1cb30e54b80234e02b35..a13308f256e217c87a03411b5ead6cde4f38e4d1 100644 (file)
@@ -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 ''
 
 
index 4a476b9ba36c6432482a57e0c1acb47487a65529..b5a5a5802ee8d3afe8d3aaaf9978bce22f0ddcc7 100644 (file)
@@ -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:
index 901be105545a9837e8c37e9a938b5ede4d700623..de72238d5c08e69949534187d18772fb8d4f214d 100644 (file)
@@ -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:
index 6d60f232292b264fcef1c7eeff290de86a57af7c..34bf0afc925609e8b5f8fee7542edc803b83aa02 100644 (file)
@@ -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)
index d7c0498e1a3738786c5c51977ece3324c3a7bfa9..898a7a848900ddc045949e20179a29b2f9e58597 100644 (file)
@@ -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',
index 24a1221ef5294620a030d3318399b6e66effe880..a75df953499ce28dccd936f71fd554046b9335ec 100644 (file)
@@ -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():
index 751f21fdc3130cef65a29f788634533880274117..f82c4953fee7aed42ee272cdb997bd84bc20fef5 100644 (file)
 
 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():
index 4366f412327f07b5f17a36de7785cf9f998a5c5b..ed0cf8f59de4e91ad8d0270235ede2a9e25e9f60 100644 (file)
 #    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 {
index 1bab2627a873566e3f321dc15a88c9a0d76de866..71f6a0cb1c41ad79b099c61a4963dcec9ec46de3 100644 (file)
 #    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))
index 70a606d93c97de3e7d50ddfea1112ffee49ed4d0..7715877bde8cb7ceacaca461c396eebba5819ae9 100644 (file)
@@ -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:
index 87286782e3b562592b2f3181362be62500d52a91..aeab875a16d70651d206ad72d09235367648ceec 100644 (file)
@@ -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')
index d0637077139ca03677a431b118ade099e3f50094..46eca8dcf1e1e2b91b80dd17e45f5e26758a0f18 100644 (file)
@@ -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),
index 8270cc8fba9623fffb298efefeb3e5f0c1f38c2c..40f5e764a336a4642ca064932b0231720111eaf3 100644 (file)
@@ -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,