]> review.fuel-infra Code Review - openstack-build/heat-build.git/commitdiff
Add resource_data table for free-form key/value data.
authorJason Dunsmore <jasondunsmore@gmail.com>
Thu, 11 Jul 2013 21:26:06 +0000 (16:26 -0500)
committerJason Dunsmore <jasondunsmore@gmail.com>
Thu, 11 Jul 2013 21:26:06 +0000 (16:26 -0500)
This can be used to store arbitrary resource-specific data, such as
SSH keys.

Blueprint resource-data.

Change-Id: Ic148bd131f528a676904c8f37fd6deb936917152

heat/db/sqlalchemy/api.py
heat/db/sqlalchemy/migrate_repo/versions/021_resource_data.py [new file with mode: 0644]
heat/db/sqlalchemy/models.py
heat/engine/resource.py
heat/tests/test_sqlalchemy_api.py [new file with mode: 0644]

index 02941bf77db0bc93a1e32e0b7b003b5cde3b0995..98e85c8a7e393291122a73fb7ea4105627f1d7a9 100644 (file)
@@ -97,6 +97,54 @@ def resource_get_all(context):
     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)
diff --git a/heat/db/sqlalchemy/migrate_repo/versions/021_resource_data.py b/heat/db/sqlalchemy/migrate_repo/versions/021_resource_data.py
new file mode 100644 (file)
index 0000000..8fe6837
--- /dev/null
@@ -0,0 +1,45 @@
+#    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()
index 780795a239afdaf584e95552c5cb53db3b066784..6ee060fb95c0882d6d2ada4673727f7dad89680f 100644 (file)
@@ -214,12 +214,30 @@ class Event(BASE, HeatBase):
     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)
@@ -234,6 +252,8 @@ class Resource(BASE, HeatBase):
                                  sqlalchemy.ForeignKey('stack.id'),
                                  nullable=False)
     stack = relationship(Stack, backref=backref('resources'))
+    data = relationship(ResourceData, backref=backref('resources',
+                                                      lazy='joined'))
 
 
 class WatchRule(BASE, HeatBase):
index 264f0c73faecc3c5e82f2d0118fc719d799e4f85..9a90f6e6cb5146dbdc6e4ca8722f7bcd88574f59 100644 (file)
@@ -179,12 +179,14 @@ class Resource(object):
             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.'''
diff --git a/heat/tests/test_sqlalchemy_api.py b/heat/tests/test_sqlalchemy_api.py
new file mode 100644 (file)
index 0000000..5782430
--- /dev/null
@@ -0,0 +1,99 @@
+#    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")