# vim: tabstop=4 shiftwidth=4 softtabstop=4 # # 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 re import urllib import urlparse import collections 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(urllib.unquote(fields[4]), urllib.unquote(path.group(1)), urllib.unquote(path.group(2)), urllib.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 = 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 = urllib.unquote(url_arn) return cls.from_arn(arn) def arn(self): ''' Return an ARN of the form: arn:openstack:heat:::stacks// ''' return 'arn:openstack:heat::%s:%s' % (urllib.quote(self.tenant, ''), self._tenant_path()) def arn_url_path(self): ''' Return an ARN quoted correctly for use in a URL ''' return '/' + urllib.quote(self.arn(), '') def url_path(self): ''' Return a URL-encoded path segment of a URL in the form: /stacks// ''' return '/'.join((urllib.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// ''' return 'stacks/%s/%s%s' % (urllib.quote(self.stack_name, ''), urllib.quote(self.stack_id, ''), urllib.quote(self.path)) 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()