return results
+def resource_data_get(resource, key):
+ """Lookup value of resource's data by key."""
+ data_lst = filter(lambda x: x.key == key, resource.data)
+ if not data_lst:
+ return None
+ assert len(data_lst) == 1
+ data = data_lst[0]
+
+ if data.redact:
+ return crypt.decrypt(data.value)
+ else:
+ return data.value
+
+
+def resource_data_set(resource, key, value, redact=False):
+ """Save resource's key/value pair to database."""
+ if redact:
+ value = crypt.encrypt(value)
+ data_lst = filter(lambda x: x.key == key, resource.data)
+
+ if data_lst: # Key exists in db, so check value & replace if necessary
+ assert len(data_lst) == 1
+ resource_data = data_lst[0]
+
+ # If the new value is the same, do nothing
+ if value == resource_data.value:
+ return None
+
+ # Otherwise, delete the old value
+ for i, d in enumerate(resource.data):
+ if d.key == key:
+ index = i
+ del(resource.data[index])
+
+ else: # Build a new key/value
+ resource_data = models.ResourceData()
+ resource_data.key = key
+ resource_data.resource_id = resource.id
+ resource_data.redact = True
+
+ resource_data.value = value
+ resource.data.append(resource_data)
+
+ # Save to new key/value pair to database
+ rs = model_query(resource.context, models.Resource).get(resource.id)
+ rs.update_and_save({'data': resource.data})
+
+
def resource_create(context, values):
resource_ref = models.Resource()
resource_ref.update(values)
--- /dev/null
+# 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 sqlalchemy
+
+
+def upgrade(migrate_engine):
+ meta = sqlalchemy.MetaData()
+ meta.bind = migrate_engine
+
+ resource_data = sqlalchemy.Table(
+ 'resource_data', meta,
+ sqlalchemy.Column('id',
+ sqlalchemy.Integer,
+ primary_key=True,
+ nullable=False),
+ sqlalchemy.Column('created_at', sqlalchemy.DateTime),
+ sqlalchemy.Column('updated_at', sqlalchemy.DateTime),
+ sqlalchemy.Column('key', sqlalchemy.String(255)),
+ sqlalchemy.Column('value', sqlalchemy.Text),
+ sqlalchemy.Column('redact', sqlalchemy.Boolean),
+ sqlalchemy.Column('resource_id',
+ sqlalchemy.String(36),
+ sqlalchemy.ForeignKey('resource.id'),
+ nullable=False)
+ )
+ sqlalchemy.Table('resource', meta, autoload=True)
+ resource_data.create()
+
+
+def downgrade(migrate_engine):
+ meta = sqlalchemy.MetaData()
+ meta.bind = migrate_engine
+
+ resource_data = sqlalchemy.Table('resource_data', meta, autoload=True)
+ resource_data.drop()
resource_properties = sqlalchemy.Column(sqlalchemy.PickleType)
+class ResourceData(BASE, HeatBase):
+ """Key/value store of arbitrary, resource-specific data."""
+
+ __tablename__ = 'resource_data'
+
+ id = sqlalchemy.Column('id',
+ sqlalchemy.Integer,
+ primary_key=True,
+ nullable=False)
+ key = sqlalchemy.Column('key', sqlalchemy.String)
+ value = sqlalchemy.Column('value', sqlalchemy.String)
+ redact = sqlalchemy.Column('redact', sqlalchemy.Boolean)
+ resource_id = sqlalchemy.Column('resource_id',
+ sqlalchemy.String,
+ sqlalchemy.ForeignKey('resource.id'),
+ nullable=False)
+
+
class Resource(BASE, HeatBase):
"""Represents a resource created by the heat engine."""
__tablename__ = 'resource'
- id = sqlalchemy.Column(sqlalchemy.Integer,
+ id = sqlalchemy.Column(sqlalchemy.String,
primary_key=True,
default=uuidutils.generate_uuid)
action = sqlalchemy.Column('action', sqlalchemy.String)
sqlalchemy.ForeignKey('stack.id'),
nullable=False)
stack = relationship(Stack, backref=backref('resources'))
+ data = relationship(ResourceData, backref=backref('resources',
+ lazy='joined'))
class WatchRule(BASE, HeatBase):
self.status = resource.status
self.status_reason = resource.status_reason
self.id = resource.id
+ self.data = resource.data
else:
self.resource_id = None
self.action = None
self.status = None
self.status_reason = ''
self.id = None
+ self.data = []
def __eq__(self, other):
'''Allow == comparison of two resources.'''
--- /dev/null
+# 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.
+
+from heat.db.sqlalchemy import api as db_api
+from heat.engine import environment
+from heat.tests.v1_1 import fakes
+from heat.engine.resource import Resource
+from heat.common import template_format
+from heat.engine import parser
+from heat.openstack.common import uuidutils
+from heat.tests.common import HeatTestCase
+from heat.tests.utils import setup_dummy_db
+
+
+wp_template = '''
+{
+ "AWSTemplateFormatVersion" : "2010-09-09",
+ "Description" : "WordPress",
+ "Parameters" : {
+ "KeyName" : {
+ "Description" : "KeyName",
+ "Type" : "String",
+ "Default" : "test"
+ }
+ },
+ "Resources" : {
+ "WebServer": {
+ "Type": "AWS::EC2::Instance",
+ "Properties": {
+ "ImageId" : "F17-x86_64-gold",
+ "InstanceType" : "m1.large",
+ "KeyName" : "test",
+ "UserData" : "wordpress"
+ }
+ }
+ }
+}
+'''
+
+
+class MyResource(Resource):
+ properties_schema = {
+ 'ServerName': {'Type': 'String', 'Required': True},
+ 'Flavor': {'Type': 'String', 'Required': True},
+ 'ImageName': {'Type': 'String', 'Required': True},
+ 'UserData': {'Type': 'String'},
+ 'PublicKey': {'Type': 'String'}
+ }
+
+ @property
+ def my_secret(self):
+ return db_api.resource_data_get(self, 'my_secret')
+
+ @my_secret.setter
+ def my_secret(self, my_secret):
+ db_api.resource_data_set(self, 'my_secret', my_secret, True)
+
+
+class SqlAlchemyTest(HeatTestCase):
+ def setUp(self):
+ super(SqlAlchemyTest, self).setUp()
+ self.fc = fakes.FakeClient()
+ setup_dummy_db()
+
+ def _setup_test_stack(self, stack_name):
+ t = template_format.parse(wp_template)
+ template = parser.Template(t)
+ stack = parser.Stack(None, stack_name, template,
+ environment.Environment({'KeyName': 'test'}),
+ stack_id=uuidutils.generate_uuid())
+ return (t, stack)
+
+ def test_encryption(self):
+ stack_name = 'test_encryption'
+ (t, stack) = self._setup_test_stack(stack_name)
+ cs = MyResource('cs_encryption',
+ t['Resources']['WebServer'],
+ stack)
+
+ # This gives the fake cloud server an id and created_time attribute
+ cs._store_or_update(cs.CREATE, cs.IN_PROGRESS, 'test_store')
+
+ cs.my_secret = 'fake secret'
+ rs = db_api.resource_get_by_name_and_stack(None,
+ 'cs_encryption',
+ stack.id)
+ encrypted_key = rs.data[0]['value']
+ self.assertNotEqual(encrypted_key, "fake secret")
+ decrypted_key = cs.my_secret
+ self.assertEqual(decrypted_key, "fake secret")