acdc77e1c9
This returns stack_name/stack_id, which is the form that needs to be used with heatclient to avoid doing an extra stack lookup API call. See the next change in this series to see this in use. Change-Id: I41dcb732d36d702b7583b5e877fd074f86445a03 Related-Bug: #1291097
248 lines
8.2 KiB
Python
248 lines
8.2 KiB
Python
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
import collections
|
|
import re
|
|
|
|
from heat.openstack.common.py3kcompat import urlutils
|
|
from heat.openstack.common import strutils
|
|
|
|
|
|
class HeatIdentifier(collections.Mapping):
|
|
|
|
FIELDS = (
|
|
TENANT, STACK_NAME, STACK_ID, PATH
|
|
) = (
|
|
'tenant', 'stack_name', 'stack_id', 'path'
|
|
)
|
|
path_re = re.compile(r'stacks/([^/]+)/([^/]+)(.*)')
|
|
|
|
def __init__(self, tenant, stack_name, stack_id, path=''):
|
|
'''
|
|
Initialise a HeatIdentifier from a Tenant ID, Stack name, Stack ID
|
|
and optional path. If a path is supplied and it does not begin with
|
|
"/", a "/" will be prepended.
|
|
'''
|
|
if path and not path.startswith('/'):
|
|
path = '/' + path
|
|
|
|
if '/' in stack_name:
|
|
raise ValueError(_('Stack name may not contain "/"'))
|
|
|
|
self.identity = {
|
|
self.TENANT: tenant,
|
|
self.STACK_NAME: stack_name,
|
|
self.STACK_ID: str(stack_id),
|
|
self.PATH: path,
|
|
}
|
|
|
|
@classmethod
|
|
def from_arn(cls, arn):
|
|
'''
|
|
Return a new HeatIdentifier generated by parsing the supplied ARN.
|
|
'''
|
|
fields = arn.split(':')
|
|
if len(fields) < 6 or fields[0].lower() != 'arn':
|
|
raise ValueError(_('"%s" is not a valid ARN') % arn)
|
|
|
|
id_fragment = ':'.join(fields[5:])
|
|
path = cls.path_re.match(id_fragment)
|
|
|
|
if fields[1] != 'openstack' or fields[2] != 'heat' or not path:
|
|
raise ValueError(_('"%s" is not a valid Heat ARN') % arn)
|
|
|
|
return cls(urlutils.unquote(fields[4]),
|
|
urlutils.unquote(path.group(1)),
|
|
urlutils.unquote(path.group(2)),
|
|
urlutils.unquote(path.group(3)))
|
|
|
|
@classmethod
|
|
def from_arn_url(cls, url):
|
|
'''
|
|
Return a new HeatIdentifier generated by parsing the supplied URL
|
|
The URL is expected to contain a valid arn as part of the path
|
|
'''
|
|
# Sanity check the URL
|
|
urlp = urlutils.urlparse(url)
|
|
if (urlp.scheme not in ('http', 'https') or
|
|
not urlp.netloc or not urlp.path):
|
|
raise ValueError(_('"%s" is not a valid URL') % url)
|
|
|
|
# Remove any query-string and extract the ARN
|
|
arn_url_prefix = '/arn%3Aopenstack%3Aheat%3A%3A'
|
|
match = re.search(arn_url_prefix, urlp.path, re.IGNORECASE)
|
|
if match is None:
|
|
raise ValueError(_('"%s" is not a valid ARN URL') % url)
|
|
# the +1 is to skip the leading /
|
|
url_arn = urlp.path[match.start() + 1:]
|
|
arn = urlutils.unquote(url_arn)
|
|
return cls.from_arn(arn)
|
|
|
|
def arn(self):
|
|
'''
|
|
Return an ARN of the form:
|
|
arn:openstack:heat::<tenant>:stacks/<stack_name>/<stack_id><path>
|
|
'''
|
|
return 'arn:openstack:heat::%s:%s' % (urlutils.quote(self.tenant, ''),
|
|
self._tenant_path())
|
|
|
|
def arn_url_path(self):
|
|
'''
|
|
Return an ARN quoted correctly for use in a URL
|
|
'''
|
|
return '/' + urlutils.quote(self.arn(), '')
|
|
|
|
def url_path(self):
|
|
'''
|
|
Return a URL-encoded path segment of a URL in the form:
|
|
<tenant>/stacks/<stack_name>/<stack_id><path>
|
|
'''
|
|
return '/'.join((urlutils.quote(self.tenant, ''), self._tenant_path()))
|
|
|
|
def _tenant_path(self):
|
|
'''
|
|
Return a URL-encoded path segment of a URL within a particular tenant,
|
|
in the form:
|
|
stacks/<stack_name>/<stack_id><path>
|
|
'''
|
|
return 'stacks/%s%s' % (self.stack_path(),
|
|
urlutils.quote(strutils.safe_encode(
|
|
self.path)))
|
|
|
|
def stack_path(self):
|
|
'''
|
|
Return a URL-encoded path segment of a URL,
|
|
in the form:
|
|
<stack_name>/<stack_id>
|
|
'''
|
|
return '%s/%s' % (urlutils.quote(self.stack_name, ''),
|
|
urlutils.quote(self.stack_id, ''))
|
|
|
|
def _path_components(self):
|
|
'''Return a list of the path components.'''
|
|
return self.path.lstrip('/').split('/')
|
|
|
|
def __getattr__(self, attr):
|
|
'''
|
|
Return one of the components of the identity when accessed as an
|
|
attribute.
|
|
'''
|
|
if attr not in self.FIELDS:
|
|
raise AttributeError(_('Unknown attribute "%s"') % attr)
|
|
|
|
return self.identity[attr]
|
|
|
|
def __getitem__(self, key):
|
|
'''Return one of the components of the identity.'''
|
|
if key not in self.FIELDS:
|
|
raise KeyError(_('Unknown attribute "%s"') % key)
|
|
|
|
return self.identity[key]
|
|
|
|
def __len__(self):
|
|
'''Return the number of components in an identity.'''
|
|
return len(self.FIELDS)
|
|
|
|
def __contains__(self, key):
|
|
return key in self.FIELDS
|
|
|
|
def __iter__(self):
|
|
return iter(self.FIELDS)
|
|
|
|
def __repr__(self):
|
|
return repr(dict(self))
|
|
|
|
|
|
class ResourceIdentifier(HeatIdentifier):
|
|
'''An identifier for a resource.'''
|
|
|
|
RESOURCE_NAME = 'resource_name'
|
|
|
|
def __init__(self, tenant, stack_name, stack_id, path,
|
|
resource_name=None):
|
|
'''
|
|
Return a new Resource identifier based on the identifier components of
|
|
the owning stack and the resource name.
|
|
'''
|
|
if resource_name is not None:
|
|
if '/' in resource_name:
|
|
raise ValueError(_('Resource name may not contain "/"'))
|
|
path = '/'.join([path.rstrip('/'), 'resources', resource_name])
|
|
super(ResourceIdentifier, self).__init__(tenant,
|
|
stack_name,
|
|
stack_id,
|
|
path)
|
|
|
|
def __getattr__(self, attr):
|
|
'''
|
|
Return one of the components of the identity when accessed as an
|
|
attribute.
|
|
'''
|
|
|
|
if attr == self.RESOURCE_NAME:
|
|
return self._path_components()[-1]
|
|
|
|
return HeatIdentifier.__getattr__(self, attr)
|
|
|
|
def stack(self):
|
|
'''
|
|
Return a HeatIdentifier for the owning stack
|
|
'''
|
|
return HeatIdentifier(self.tenant, self.stack_name, self.stack_id,
|
|
'/'.join(self._path_components()[:-2]))
|
|
|
|
|
|
class EventIdentifier(HeatIdentifier):
|
|
'''An identifier for an event.'''
|
|
|
|
(RESOURCE_NAME, EVENT_ID) = (ResourceIdentifier.RESOURCE_NAME, 'event_id')
|
|
|
|
def __init__(self, tenant, stack_name, stack_id, path,
|
|
event_id=None):
|
|
'''
|
|
Return a new Event identifier based on the identifier components of
|
|
the associated resource and the event ID.
|
|
'''
|
|
if event_id is not None:
|
|
path = '/'.join([path.rstrip('/'), 'events', event_id])
|
|
super(EventIdentifier, self).__init__(tenant,
|
|
stack_name,
|
|
stack_id,
|
|
path)
|
|
|
|
def __getattr__(self, attr):
|
|
'''
|
|
Return one of the components of the identity when accessed as an
|
|
attribute.
|
|
'''
|
|
|
|
if attr == self.RESOURCE_NAME:
|
|
return getattr(self.resource(), attr)
|
|
if attr == self.EVENT_ID:
|
|
return self._path_components()[-1]
|
|
|
|
return HeatIdentifier.__getattr__(self, attr)
|
|
|
|
def resource(self):
|
|
'''
|
|
Return a HeatIdentifier for the owning resource
|
|
'''
|
|
return ResourceIdentifier(self.tenant, self.stack_name, self.stack_id,
|
|
'/'.join(self._path_components()[:-2]))
|
|
|
|
def stack(self):
|
|
'''
|
|
Return a HeatIdentifier for the owning stack
|
|
'''
|
|
return self.resource().stack()
|