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: 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
This commit is contained in:
parent
1ef28a3706
commit
a5510ea245
@ -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,
|
||||
|
@ -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
|
||||
|
108
heat/engine/s3.py
Normal file
108
heat/engine/s3.py
Normal 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
|
||||
#
|
||||
# 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)
|
30
templates/S3_Single_Instance.template
Normal file
30
templates/S3_Single_Instance.template
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
@ -26,4 +26,5 @@ WebOb
|
||||
python-keystoneclient
|
||||
glance
|
||||
python-memcached
|
||||
python-swiftclient
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user