summaryrefslogtreecommitdiffstats
path: root/meta-openstack/recipes-devtools/python
diff options
context:
space:
mode:
authorBruce Ashfield <bruce.ashfield@windriver.com>2014-01-08 00:50:15 -0500
committerBruce Ashfield <bruce.ashfield@windriver.com>2014-01-15 00:33:53 -0500
commitb45880a1a11007476446c7e2f53b0fee43c43453 (patch)
tree160739eceae26c926c0b93ebaf5a039788f025ee /meta-openstack/recipes-devtools/python
parent9ee16ea7922a1777e95cd5ecdf577c6d5935bbd2 (diff)
downloadmeta-cloud-services-b45880a1a11007476446c7e2f53b0fee43c43453.tar.gz
ceilometer: postgresql fixes
Cherry picking two ceilometer master changes to address postgresql database issues: https://bugs.launchpad.net/ceilometer/+bug/1241526 https://review.openstack.org/#/c/49456/ Signed-off-by: Bruce Ashfield <bruce.ashfield@windriver.com>
Diffstat (limited to 'meta-openstack/recipes-devtools/python')
-rw-r--r--meta-openstack/recipes-devtools/python/python-ceilometer/0001-Fix-for-get_resources-with-postgresql.patch36
-rw-r--r--meta-openstack/recipes-devtools/python/python-ceilometer/0002-enable-sql-metadata-query.patch419
-rw-r--r--meta-openstack/recipes-devtools/python/python-ceilometer_git.bb2
3 files changed, 457 insertions, 0 deletions
diff --git a/meta-openstack/recipes-devtools/python/python-ceilometer/0001-Fix-for-get_resources-with-postgresql.patch b/meta-openstack/recipes-devtools/python/python-ceilometer/0001-Fix-for-get_resources-with-postgresql.patch
new file mode 100644
index 0000000..52cffb3
--- /dev/null
+++ b/meta-openstack/recipes-devtools/python/python-ceilometer/0001-Fix-for-get_resources-with-postgresql.patch
@@ -0,0 +1,36 @@
1From f28a381b58516018ca35cdf7b4e2879a5bcac6ad Mon Sep 17 00:00:00 2001
2From: Thomas Maddox <thomas.maddox@rackspace.com>
3Date: Mon, 21 Oct 2013 15:55:49 +0000
4Subject: [PATCH 1/2] Fix for get_resources with postgresql
5
6Add max_ts and min_ts to GROUP BY in sub-query, since they need to be aggregated to SELECT them.
7
8Closes-Bug: #1241526
9Change-Id: Ifdd2bc661b5da31bd40d1c3fa1fc442d7417399f
10(cherry picked from commit 0a98159bc9c727a89bd8c15347ac380a21acaa59)
11
12Signed-off-by: Bruce Ashfield <bruce.ashfield@windriver.com>
13---
14 ceilometer/storage/impl_sqlalchemy.py | 6 +++++-
15 1 file changed, 5 insertions(+), 1 deletion(-)
16
17diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py
18index a6e7d307e407..546c0c0e6553 100644
19--- a/ceilometer/storage/impl_sqlalchemy.py
20+++ b/ceilometer/storage/impl_sqlalchemy.py
21@@ -361,7 +361,11 @@ class Connection(base.Connection):
22 ).filter(
23 Meter.resource_id == ts_subquery.c.resource_id,
24 Meter.timestamp == ts_subquery.c.max_ts
25- ).group_by(Meter.resource_id).subquery()
26+ ).group_by(
27+ ts_subquery.c.resource_id,
28+ ts_subquery.c.max_ts,
29+ ts_subquery.c.min_ts
30+ ).subquery()
31
32 query = session.query(
33 Meter,
34--
351.7.10.4
36
diff --git a/meta-openstack/recipes-devtools/python/python-ceilometer/0002-enable-sql-metadata-query.patch b/meta-openstack/recipes-devtools/python/python-ceilometer/0002-enable-sql-metadata-query.patch
new file mode 100644
index 0000000..c96c631
--- /dev/null
+++ b/meta-openstack/recipes-devtools/python/python-ceilometer/0002-enable-sql-metadata-query.patch
@@ -0,0 +1,419 @@
1From 35488d1099c634d88d7e6c262eb9a6636ee2d7d8 Mon Sep 17 00:00:00 2001
2From: Gordon Chung <chungg@ca.ibm.com>
3Date: Wed, 2 Oct 2013 15:45:26 -0400
4Subject: [PATCH 2/2] enable sql metadata query
5
6explode metadata key/values to their own tables/rows (based on type).
7build a key string using dot notation similar to other nosql db
8and filter based on that.
9
10Blueprint: sqlalchemy-metadata-query
11Related-Bug: #1093625
12
13Change-Id: I2076e67b79448f98124a57b62b5bfed7aa8ae2ad
14(cherry picked from commit 1570462507eae1478123de25dbadc64b09c82af3)
15
16Signed-off-by: Bruce Ashfield <bruce.ashfield@windriver.com>
17---
18 ceilometer/storage/impl_sqlalchemy.py | 79 +++++++++++++++++---
19 .../versions/020_add_metadata_tables.py | 78 +++++++++++++++++++
20 ceilometer/storage/sqlalchemy/models.py | 48 ++++++++++++
21 ceilometer/utils.py | 24 ++++++
22 doc/source/install/dbreco.rst | 4 +-
23 tests/api/v2/test_list_meters_scenarios.py | 1 +
24 tests/test_utils.py | 16 ++++
25 7 files changed, 239 insertions(+), 11 deletions(-)
26 create mode 100644 ceilometer/storage/sqlalchemy/migrate_repo/versions/020_add_metadata_tables.py
27
28diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py
29index 546c0c0e6553..8d321eaaeffe 100644
30--- a/ceilometer/storage/impl_sqlalchemy.py
31+++ b/ceilometer/storage/impl_sqlalchemy.py
32@@ -18,10 +18,12 @@
33 """SQLAlchemy storage backend."""
34
35 from __future__ import absolute_import
36-
37 import datetime
38 import operator
39 import os
40+import types
41+
42+from sqlalchemy import and_
43 from sqlalchemy import func
44 from sqlalchemy import desc
45 from sqlalchemy.orm import aliased
46@@ -39,6 +41,10 @@ from ceilometer.storage.sqlalchemy.models import AlarmChange
47 from ceilometer.storage.sqlalchemy.models import Base
48 from ceilometer.storage.sqlalchemy.models import Event
49 from ceilometer.storage.sqlalchemy.models import Meter
50+from ceilometer.storage.sqlalchemy.models import MetaBool
51+from ceilometer.storage.sqlalchemy.models import MetaFloat
52+from ceilometer.storage.sqlalchemy.models import MetaInt
53+from ceilometer.storage.sqlalchemy.models import MetaText
54 from ceilometer.storage.sqlalchemy.models import Project
55 from ceilometer.storage.sqlalchemy.models import Resource
56 from ceilometer.storage.sqlalchemy.models import Source
57@@ -100,7 +106,40 @@ class SQLAlchemyStorage(base.StorageEngine):
58 return Connection(conf)
59
60
61-def make_query_from_filter(query, sample_filter, require_meter=True):
62+META_TYPE_MAP = {bool: MetaBool,
63+ str: MetaText,
64+ unicode: MetaText,
65+ types.NoneType: MetaText,
66+ int: MetaInt,
67+ long: MetaInt,
68+ float: MetaFloat}
69+
70+
71+def apply_metaquery_filter(session, query, metaquery):
72+ """Apply provided metaquery filter to existing query.
73+
74+ :param session: session used for original query
75+ :param query: Query instance
76+ :param metaquery: dict with metadata to match on.
77+ """
78+
79+ for k, v in metaquery.iteritems():
80+ key = k[9:] # strip out 'metadata.' prefix
81+ try:
82+ _model = META_TYPE_MAP[type(v)]
83+ except KeyError:
84+ raise NotImplementedError(_('Query on %(key)s is of %(value)s '
85+ 'type and is not supported') %
86+ {"key": k, "value": type(v)})
87+ else:
88+ meta_q = session.query(_model).\
89+ filter(and_(_model.meta_key == key,
90+ _model.value == v)).subquery()
91+ query = query.filter_by(id=meta_q.c.id)
92+ return query
93+
94+
95+def make_query_from_filter(session, query, sample_filter, require_meter=True):
96 """Return a query dictionary based on the settings in the filter.
97
98 :param filter: SampleFilter instance
99@@ -134,7 +173,8 @@ def make_query_from_filter(query, sample_filter, require_meter=True):
100 query = query.filter_by(resource_id=sample_filter.resource)
101
102 if sample_filter.metaquery:
103- raise NotImplementedError(_('metaquery not implemented'))
104+ query = apply_metaquery_filter(session, query,
105+ sample_filter.metaquery)
106
107 return query
108
109@@ -229,6 +269,21 @@ class Connection(base.Connection):
110 meter.message_signature = data['message_signature']
111 meter.message_id = data['message_id']
112
113+ if rmetadata:
114+ if isinstance(rmetadata, dict):
115+ for key, v in utils.dict_to_keyval(rmetadata):
116+ try:
117+ _model = META_TYPE_MAP[type(v)]
118+ except KeyError:
119+ LOG.warn(_("Unknown metadata type. Key (%s) will "
120+ "not be queryable."), key)
121+ else:
122+ session.add(_model(id=meter.id,
123+ meta_key=key,
124+ value=v))
125+
126+ session.flush()
127+
128 @staticmethod
129 def clear_expired_metering_data(ttl):
130 """Clear expired data from the backend storage system according to the
131@@ -306,8 +361,6 @@ class Connection(base.Connection):
132 # just fail.
133 if pagination:
134 raise NotImplementedError(_('Pagination not implemented'))
135- if metaquery:
136- raise NotImplementedError(_('metaquery not implemented'))
137
138 # (thomasm) We need to get the max timestamp first, since that's the
139 # most accurate. We also need to filter down in the subquery to
140@@ -331,6 +384,11 @@ class Connection(base.Connection):
141 ts_subquery = ts_subquery.filter(
142 Meter.sources.any(id=source))
143
144+ if metaquery:
145+ ts_subquery = apply_metaquery_filter(session,
146+ ts_subquery,
147+ metaquery)
148+
149 # Here we limit the samples being used to a specific time period,
150 # if requested.
151 if start_timestamp:
152@@ -409,8 +467,6 @@ class Connection(base.Connection):
153
154 if pagination:
155 raise NotImplementedError(_('Pagination not implemented'))
156- if metaquery:
157- raise NotImplementedError(_('metaquery not implemented'))
158
159 session = sqlalchemy_session.get_session()
160
161@@ -434,6 +490,11 @@ class Connection(base.Connection):
162 query_meter = session.query(Meter).\
163 join(subquery_meter, Meter.id == subquery_meter.c.id)
164
165+ if metaquery:
166+ query_meter = apply_metaquery_filter(session,
167+ query_meter,
168+ metaquery)
169+
170 alias_meter = aliased(Meter, query_meter.subquery())
171 query = session.query(Resource, alias_meter).join(
172 alias_meter, Resource.id == alias_meter.resource_id)
173@@ -469,7 +530,7 @@ class Connection(base.Connection):
174
175 session = sqlalchemy_session.get_session()
176 query = session.query(Meter)
177- query = make_query_from_filter(query, sample_filter,
178+ query = make_query_from_filter(session, query, sample_filter,
179 require_meter=False)
180 if limit:
181 query = query.limit(limit)
182@@ -521,7 +582,7 @@ class Connection(base.Connection):
183 if groupby:
184 query = query.group_by(*group_attributes)
185
186- return make_query_from_filter(query, sample_filter)
187+ return make_query_from_filter(session, query, sample_filter)
188
189 @staticmethod
190 def _stats_result_to_model(result, period, period_start,
191diff --git a/ceilometer/storage/sqlalchemy/migrate_repo/versions/020_add_metadata_tables.py b/ceilometer/storage/sqlalchemy/migrate_repo/versions/020_add_metadata_tables.py
192new file mode 100644
193index 000000000000..085cd6b8f398
194--- /dev/null
195+++ b/ceilometer/storage/sqlalchemy/migrate_repo/versions/020_add_metadata_tables.py
196@@ -0,0 +1,78 @@
197+#
198+# Copyright 2013 OpenStack Foundation
199+# All Rights Reserved.
200+#
201+# Licensed under the Apache License, Version 2.0 (the "License"); you may
202+# not use this file except in compliance with the License. You may obtain
203+# a copy of the License at
204+#
205+# http://www.apache.org/licenses/LICENSE-2.0
206+#
207+# Unless required by applicable law or agreed to in writing, software
208+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
209+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
210+# License for the specific language governing permissions and limitations
211+# under the License.
212+import json
213+
214+from sqlalchemy import Boolean
215+from sqlalchemy import Column
216+from sqlalchemy import Float
217+from sqlalchemy import ForeignKey
218+from sqlalchemy import Integer
219+from sqlalchemy import MetaData
220+from sqlalchemy import String
221+from sqlalchemy import Table
222+from sqlalchemy import Text
223+from sqlalchemy.sql import select
224+
225+from ceilometer import utils
226+
227+tables = [('metadata_text', Text, True),
228+ ('metadata_bool', Boolean, False),
229+ ('metadata_int', Integer, False),
230+ ('metadata_float', Float, False)]
231+
232+
233+def upgrade(migrate_engine):
234+ meta = MetaData(bind=migrate_engine)
235+ meter = Table('meter', meta, autoload=True)
236+ meta_tables = {}
237+ for t_name, t_type, t_nullable in tables:
238+ meta_tables[t_name] = Table(
239+ t_name, meta,
240+ Column('id', Integer, ForeignKey('meter.id'), primary_key=True),
241+ Column('meta_key', String(255), index=True, primary_key=True),
242+ Column('value', t_type, nullable=t_nullable),
243+ mysql_engine='InnoDB',
244+ mysql_charset='utf8',
245+ )
246+ meta_tables[t_name].create()
247+
248+ for row in select([meter]).execute():
249+ meter_id = row['id']
250+ rmeta = json.loads(row['resource_metadata'])
251+ for key, v in utils.dict_to_keyval(rmeta):
252+ if isinstance(v, basestring) or v is None:
253+ meta_tables['metadata_text'].insert().values(id=meter_id,
254+ meta_key=key,
255+ value=v)
256+ elif isinstance(v, bool):
257+ meta_tables['metadata_bool'].insert().values(id=meter_id,
258+ meta_key=key,
259+ value=v)
260+ elif isinstance(v, (int, long)):
261+ meta_tables['metadata_int'].insert().values(id=meter_id,
262+ meta_key=key,
263+ value=v)
264+ elif isinstance(v, float):
265+ meta_tables['metadata_float'].insert().values(id=meter_id,
266+ meta_key=key,
267+ value=v)
268+
269+
270+def downgrade(migrate_engine):
271+ meta = MetaData(bind=migrate_engine)
272+ for t in tables:
273+ table = Table(t[0], meta, autoload=True)
274+ table.drop()
275diff --git a/ceilometer/storage/sqlalchemy/models.py b/ceilometer/storage/sqlalchemy/models.py
276index 45f98cb59553..8f890b3056d8 100644
277--- a/ceilometer/storage/sqlalchemy/models.py
278+++ b/ceilometer/storage/sqlalchemy/models.py
279@@ -141,6 +141,54 @@ class Source(Base):
280 id = Column(String(255), primary_key=True)
281
282
283+class MetaText(Base):
284+ """Metering text metadata."""
285+
286+ __tablename__ = 'metadata_text'
287+ __table_args__ = (
288+ Index('ix_meta_text_key', 'meta_key'),
289+ )
290+ id = Column(Integer, ForeignKey('meter.id'), primary_key=True)
291+ meta_key = Column(String(255), primary_key=True)
292+ value = Column(Text)
293+
294+
295+class MetaBool(Base):
296+ """Metering boolean metadata."""
297+
298+ __tablename__ = 'metadata_bool'
299+ __table_args__ = (
300+ Index('ix_meta_bool_key', 'meta_key'),
301+ )
302+ id = Column(Integer, ForeignKey('meter.id'), primary_key=True)
303+ meta_key = Column(String(255), primary_key=True)
304+ value = Column(Boolean)
305+
306+
307+class MetaInt(Base):
308+ """Metering integer metadata."""
309+
310+ __tablename__ = 'metadata_int'
311+ __table_args__ = (
312+ Index('ix_meta_int_key', 'meta_key'),
313+ )
314+ id = Column(Integer, ForeignKey('meter.id'), primary_key=True)
315+ meta_key = Column(String(255), primary_key=True)
316+ value = Column(Integer, default=False)
317+
318+
319+class MetaFloat(Base):
320+ """Metering float metadata."""
321+
322+ __tablename__ = 'metadata_float'
323+ __table_args__ = (
324+ Index('ix_meta_float_key', 'meta_key'),
325+ )
326+ id = Column(Integer, ForeignKey('meter.id'), primary_key=True)
327+ meta_key = Column(String(255), primary_key=True)
328+ value = Column(Float, default=False)
329+
330+
331 class Meter(Base):
332 """Metering data."""
333
334diff --git a/ceilometer/utils.py b/ceilometer/utils.py
335index d5ca45f9654b..a00de72da8c5 100644
336--- a/ceilometer/utils.py
337+++ b/ceilometer/utils.py
338@@ -81,3 +81,27 @@ def stringify_timestamps(data):
339 isa_timestamp = lambda v: isinstance(v, datetime.datetime)
340 return dict((k, v.isoformat() if isa_timestamp(v) else v)
341 for (k, v) in data.iteritems())
342+
343+
344+def dict_to_keyval(value, key_base=None):
345+ """Expand a given dict to its corresponding key-value pairs.
346+
347+ Generated keys are fully qualified, delimited using dot notation.
348+ ie. key = 'key.child_key.grandchild_key[0]'
349+ """
350+ val_iter, key_func = None, None
351+ if isinstance(value, dict):
352+ val_iter = value.iteritems()
353+ key_func = lambda k: key_base + '.' + k if key_base else k
354+ elif isinstance(value, (tuple, list)):
355+ val_iter = enumerate(value)
356+ key_func = lambda k: key_base + '[%d]' % k
357+
358+ if val_iter:
359+ for k, v in val_iter:
360+ key_gen = key_func(k)
361+ if isinstance(v, dict) or isinstance(v, (tuple, list)):
362+ for key_gen, v in dict_to_keyval(v, key_gen):
363+ yield key_gen, v
364+ else:
365+ yield key_gen, v
366diff --git a/doc/source/install/dbreco.rst b/doc/source/install/dbreco.rst
367index fe6032990ade..249cdc7d92c7 100644
368--- a/doc/source/install/dbreco.rst
369+++ b/doc/source/install/dbreco.rst
370@@ -43,8 +43,8 @@ The following is a table indicating the status of each database drivers:
371 Driver API querying API statistics Alarms
372 ================== ============================= =================== ======
373 MongoDB Yes Yes Yes
374-MySQL Yes, except metadata querying Yes Yes
375-PostgreSQL Yes, except metadata querying Yes Yes
376+MySQL Yes Yes Yes
377+PostgreSQL Yes Yes Yes
378 HBase Yes Yes, except groupby No
379 DB2 Yes Yes No
380 ================== ============================= =================== ======
381diff --git a/tests/api/v2/test_list_meters_scenarios.py b/tests/api/v2/test_list_meters_scenarios.py
382index fe2c5b78db8f..3381e15dadc2 100644
383--- a/tests/api/v2/test_list_meters_scenarios.py
384+++ b/tests/api/v2/test_list_meters_scenarios.py
385@@ -252,6 +252,7 @@ class TestListMeters(FunctionalTest,
386 set(['meter.mine']))
387 self.assertEqual(set(r['resource_metadata']['is_public'] for r
388 in data), set(['False']))
389+ # FIXME(gordc): verify no false positive (Bug#1236496)
390
391 def test_list_meters_query_string_metadata(self):
392 data = self.get_json('/meters/meter.test',
393diff --git a/tests/test_utils.py b/tests/test_utils.py
394index e5185abb11b0..a14c657554f0 100644
395--- a/tests/test_utils.py
396+++ b/tests/test_utils.py
397@@ -70,3 +70,19 @@ class TestUtils(tests_base.TestCase):
398
399 def test_decimal_to_dt_with_none_parameter(self):
400 self.assertEqual(utils.decimal_to_dt(None), None)
401+
402+ def test_dict_to_kv(self):
403+ data = {'a': 'A',
404+ 'b': 'B',
405+ 'nested': {'a': 'A',
406+ 'b': 'B',
407+ },
408+ 'nested2': [{'c': 'A'}, {'c': 'B'}]
409+ }
410+ pairs = list(utils.dict_to_keyval(data))
411+ self.assertEqual(pairs, [('a', 'A'),
412+ ('b', 'B'),
413+ ('nested2[0].c', 'A'),
414+ ('nested2[1].c', 'B'),
415+ ('nested.a', 'A'),
416+ ('nested.b', 'B')])
417--
4181.7.10.4
419
diff --git a/meta-openstack/recipes-devtools/python/python-ceilometer_git.bb b/meta-openstack/recipes-devtools/python/python-ceilometer_git.bb
index e727200..e35e67e 100644
--- a/meta-openstack/recipes-devtools/python/python-ceilometer_git.bb
+++ b/meta-openstack/recipes-devtools/python/python-ceilometer_git.bb
@@ -10,6 +10,8 @@ SRCNAME = "ceilometer"
10SRC_URI = "git://github.com/openstack/${SRCNAME}.git;branch=stable/havana \ 10SRC_URI = "git://github.com/openstack/${SRCNAME}.git;branch=stable/havana \
11 file://ceilometer.conf \ 11 file://ceilometer.conf \
12 file://ceilometer.init \ 12 file://ceilometer.init \
13 file://0001-Fix-for-get_resources-with-postgresql.patch \
14 file://0002-enable-sql-metadata-query.patch \
13" 15"
14 16
15SRCREV="4d15cc05e9d2ada01b90e1d3c15427608cc49f54" 17SRCREV="4d15cc05e9d2ada01b90e1d3c15427608cc49f54"