diff --git a/heat/engine/resource_types.py b/heat/engine/resource_types.py index be2646cd3f..4126e96572 100644 --- a/heat/engine/resource_types.py +++ b/heat/engine/resource_types.py @@ -26,6 +26,7 @@ from heat.engine import dbinstance from heat.engine import eip from heat.engine import instance from heat.engine import loadbalancer +from heat.engine import s3 from heat.engine import security_group from heat.engine import stack from heat.engine import user @@ -46,6 +47,7 @@ _resource_classes = { 'AWS::EC2::Volume': volume.Volume, 'AWS::EC2::VolumeAttachment': volume.VolumeAttachment, 'AWS::ElasticLoadBalancing::LoadBalancer': loadbalancer.LoadBalancer, + 'AWS::S3::Bucket': s3.S3Bucket, 'AWS::IAM::User': user.User, 'AWS::IAM::AccessKey': user.AccessKey, 'HEAT::HA::Restarter': instance.Restarter, diff --git a/heat/engine/resources.py b/heat/engine/resources.py index df94228396..5c39033a8b 100644 --- a/heat/engine/resources.py +++ b/heat/engine/resources.py @@ -18,6 +18,7 @@ from datetime import datetime from novaclient.v1_1 import client as nc from keystoneclient.v2_0 import client as kc +from swiftclient import client as swiftclient from heat.common import exception from heat.common import config @@ -159,6 +160,7 @@ class Resource(object): self.id = None self._nova = {} self._keystone = None + self._swift = None def __eq__(self, other): '''Allow == comparison of two resources''' @@ -227,6 +229,17 @@ class Resource(object): service_name=None) return self._nova[service_type] + def swift(self): + if self._swift: + return self._swift + + con = self.context + self._swift = swiftclient.Connection( + con.auth_url, con.username, con.password, + tenant_name=con.tenant, auth_version='2') + + return self._swift + def calculate_properties(self): for p, v in self.parsed_template('Properties').items(): self.properties[p] = v diff --git a/heat/engine/s3.py b/heat/engine/s3.py new file mode 100644 index 0000000000..37271fb6b8 --- /dev/null +++ b/heat/engine/s3.py @@ -0,0 +1,108 @@ +# 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 binascii +import os +from urlparse import urlparse + +from heat.common import exception +from heat.engine.resources import Resource +from heat.openstack.common import log as logging +from swiftclient.client import ClientException + +logger = logging.getLogger('heat.engine.s3') + + +class S3Bucket(Resource): + website_schema = {'IndexDocument': {'Type': 'String'}, + 'ErrorDocument': {'Type': 'String'}} + properties_schema = {'AccessControl': { + 'Type': 'String', + 'AllowedValues': ['Private', + 'PublicRead', + 'PublicReadWrite', + 'AuthenticatedRead', + 'BucketOwnerRead', + 'BucketOwnerFullControl']}, + 'DeletionPolicy': { + 'Type': 'String', + 'AllowedValues': ['Delete', + 'Retain']}, + 'WebsiteConfiguration': {'Type': 'Map', + 'Schema': website_schema}} + + def __init__(self, name, json_snippet, stack): + super(S3Bucket, self).__init__(name, json_snippet, stack) + + def handle_create(self): + """Create a bucket.""" + container = 'heat-%s-%s' % (self.name, + binascii.hexlify(os.urandom(10))) + headers = {} + logger.debug('S3Bucket create container %s with headers %s' % + (container, headers)) + if 'WebsiteConfiguration' in self.properties: + site_cfg = self.properties['WebsiteConfiguration'] + # we will assume that swift is configured for the staticweb + # wsgi middleware + headers['X-Container-Meta-Web-Index'] = site_cfg['IndexDocument'] + headers['X-Container-Meta-Web-Error'] = site_cfg['ErrorDocument'] + + con = self.context + ac = self.properties['AccessControl'] + tenant_username = '%s:%s' % (con.tenant, con.username) + if ac in ('PublicRead', 'PublicReadWrite'): + headers['X-Container-Read'] = '.r:*' + elif ac == 'AuthenticatedRead': + headers['X-Container-Read'] = con.tenant + else: + headers['X-Container-Read'] = tenant_username + + if ac == 'PublicReadWrite': + headers['X-Container-Write'] = '.r:*' + else: + headers['X-Container-Write'] = tenant_username + + self.swift().put_container(container, headers) + self.instance_id_set(container) + + def handle_update(self): + return self.UPDATE_REPLACE + + def handle_delete(self): + """Perform specified delete policy""" + if self.properties['DeletionPolicy'] == 'Retain': + return + logger.debug('S3Bucket delete container %s' % self.instance_id) + if self.instance_id is not None: + try: + self.swift().delete_container(self.instance_id) + except ClientException as ex: + logger.warn("Delete container failed: %s" % str(ex)) + + def FnGetRefId(self): + return unicode(self.instance_id) + + def FnGetAtt(self, key): + url, token_id = self.swift().get_auth() + parsed = list(urlparse(url)) + if key == 'DomainName': + return parsed[1].split(':')[0] + elif key == 'WebsiteURL': + return '%s://%s%s/%s' % (parsed[0], parsed[1], parsed[2], + self.instance_id) + else: + raise exception.InvalidTemplateAttribute(resource=self.name, + key=key) diff --git a/templates/S3_Single_Instance.template b/templates/S3_Single_Instance.template new file mode 100644 index 0000000000..33f4425507 --- /dev/null +++ b/templates/S3_Single_Instance.template @@ -0,0 +1,30 @@ +{ + "AWSTemplateFormatVersion" : "2010-09-09", + + "Description" : "Template to test S3 Bucket resources", + + "Resources" : { + "S3Bucket" : { + "Type" : "AWS::S3::Bucket", + "Properties" : { + "AccessControl" : "PublicRead", + "WebsiteConfiguration" : { + "IndexDocument" : "index.html", + "ErrorDocument" : "error.html" + }, + "DeletionPolicy" : "Delete" + } + } + }, + + "Outputs" : { + "WebsiteURL" : { + "Value" : { "Fn::GetAtt" : [ "S3Bucket", "WebsiteURL" ] }, + "Description" : "URL for website hosted on S3" + }, + "DomainName" : { + "Value" : { "Fn::GetAtt" : [ "S3Bucket", "DomainName" ] }, + "Description" : "Domain of S3 host" + } + } +} diff --git a/tools/pip-requires b/tools/pip-requires index b62a35e280..092419da8d 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -26,4 +26,5 @@ WebOb python-keystoneclient glance python-memcached +python-swiftclient