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:
parent
42de0c9a3c
commit
cf9c45a40e
|
@ -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:
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 ''
|
||||
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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():
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue