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-<resource name>-<random hex>. eg:

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
This commit is contained in:
Steve Baker 2012-09-11 14:58:34 +12:00
parent 1ef28a3706
commit a5510ea245
5 changed files with 154 additions and 0 deletions

View File

@ -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,

View File

@ -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): = 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):
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():[p] = v

heat/engine/ Normal file
View File

@ -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
# 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',
'DeletionPolicy': {
'Type': 'String',
'AllowedValues': ['Delete',
'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' % (,
headers = {}
logger.debug('S3Bucket create container %s with headers %s' %
(container, headers))
if 'WebsiteConfiguration' in
site_cfg =['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 =['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
headers['X-Container-Read'] = tenant_username
if ac == 'PublicReadWrite':
headers['X-Container-Write'] = '.r:*'
headers['X-Container-Write'] = tenant_username
self.swift().put_container(container, headers)
def handle_update(self):
return self.UPDATE_REPLACE
def handle_delete(self):
"""Perform specified delete policy"""
if['DeletionPolicy'] == 'Retain':
logger.debug('S3Bucket delete container %s' % self.instance_id)
if self.instance_id is not None:
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],
raise exception.InvalidTemplateAttribute(,

View File

@ -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"

View File

@ -26,4 +26,5 @@ WebOb