a99fe6a760
See https://blueprints.launchpad.net/oslo-incubator/+spec/drop-namespace-packages Closes-Bug: 1423174 Change-Id: I48680ce6e7ce91005c147ab4388203946171d433
238 lines
8.1 KiB
Python
238 lines
8.1 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 oslo_utils import encodeutils
|
|
from six.moves.urllib import parse as urlparse
|
|
|
|
from heat.common.i18n import _
|
|
|
|
|
|
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.
|
|
|
|
Identifier is initialized 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):
|
|
"""Generate a new HeatIdentifier 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(urlparse.unquote(fields[4]),
|
|
urlparse.unquote(path.group(1)),
|
|
urlparse.unquote(path.group(2)),
|
|
urlparse.unquote(path.group(3)))
|
|
|
|
@classmethod
|
|
def from_arn_url(cls, url):
|
|
"""Generate a new HeatIdentifier by parsing the supplied URL.
|
|
|
|
The URL is expected to contain a valid arn as part of the path.
|
|
"""
|
|
# Sanity check the URL
|
|
urlp = urlparse.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 = urlparse.unquote(url_arn)
|
|
return cls.from_arn(arn)
|
|
|
|
def arn(self):
|
|
"""Return as an ARN.
|
|
|
|
Returned in the form:
|
|
arn:openstack:heat::<tenant>:stacks/<stack_name>/<stack_id><path>
|
|
"""
|
|
return 'arn:openstack:heat::%s:%s' % (urlparse.quote(self.tenant, ''),
|
|
self._tenant_path())
|
|
|
|
def arn_url_path(self):
|
|
"""Return an ARN quoted correctly for use in a URL."""
|
|
return '/' + urlparse.quote(self.arn(), '')
|
|
|
|
def url_path(self):
|
|
"""Return a URL-encoded path segment of a URL.
|
|
|
|
Returned in the form:
|
|
<tenant>/stacks/<stack_name>/<stack_id><path>
|
|
"""
|
|
return '/'.join((urlparse.quote(self.tenant, ''), self._tenant_path()))
|
|
|
|
def _tenant_path(self):
|
|
"""URL-encoded path segment of a URL within a particular tenant.
|
|
|
|
Returned in the form:
|
|
stacks/<stack_name>/<stack_id><path>
|
|
"""
|
|
return 'stacks/%s%s' % (self.stack_path(),
|
|
urlparse.quote(encodeutils.safe_encode(
|
|
self.path)))
|
|
|
|
def stack_path(self):
|
|
"""Return a URL-encoded path segment of a URL without a tenant.
|
|
|
|
Returned in the form:
|
|
<stack_name>/<stack_id>
|
|
"""
|
|
return '%s/%s' % (urlparse.quote(self.stack_name, ''),
|
|
urlparse.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 a component 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):
|
|
"""Initialise a new Resource identifier.
|
|
|
|
The identifier is 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 a component 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):
|
|
"""Initialise a new Event identifier based on components.
|
|
|
|
The identifier is 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 a component 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()
|