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
This commit is contained in:
Randall Burt 2013-06-13 12:58:29 -05:00
parent 42de0c9a3c
commit cf9c45a40e
17 changed files with 150 additions and 102 deletions

View 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:

View 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():

View 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):

View 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':
elif name in ['PublicIp', 'PrivateIp', 'PublicDnsName',
'PrivateDnsName']:
res = self._ipaddress()
elif key == 'PrivateIp':
res = self._ipaddress()
elif key == 'PublicDnsName':
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:

View 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 ''

View 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:

View 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:

View 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)

View 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',

View 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():

View File

@ -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():

View File

@ -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 {

View File

@ -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))

View 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:

View 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')

View 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),

View 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,