From a5510ea245b9f291a2a4a496c06aeb3c71322c9c Mon Sep 17 00:00:00 2001 From: Steve Baker Date: Tue, 11 Sep 2012 14:58:34 +1200 Subject: [PATCH] Implement the AWS::S3::Bucket resource type. An attempt was made to make created bucket names readable and unique. Names are of the format heat--. eg: heat-S3Bucket-b420d12d02e5d6e46f13 Only the swift v2 auth is currently supported, which means swift will need to use keystone for auth. This may be a valid assumption for any environment that is running Heat. When DeletionPolicy is Delete then an attempt is made to delete the container, but the stack will still be deleted if container delete fails. Run the template S3_Single_Instance.template to give it a try. Functional tests will be coming in a later change. Change-Id: Ifa2c3c4fcbdb00a44f8c6b347a61f8e1735e8328 --- heat/engine/resource_types.py | 2 + heat/engine/resources.py | 13 ++++ heat/engine/s3.py | 108 ++++++++++++++++++++++++++ templates/S3_Single_Instance.template | 30 +++++++ tools/pip-requires | 1 + 5 files changed, 154 insertions(+) create mode 100644 heat/engine/s3.py create mode 100644 templates/S3_Single_Instance.template 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