diff options
author | Bruce Ashfield <bruce.ashfield@windriver.com> | 2014-01-08 00:50:15 -0500 |
---|---|---|
committer | Bruce Ashfield <bruce.ashfield@windriver.com> | 2014-01-15 00:33:53 -0500 |
commit | b45880a1a11007476446c7e2f53b0fee43c43453 (patch) | |
tree | 160739eceae26c926c0b93ebaf5a039788f025ee /meta-openstack/recipes-devtools/python | |
parent | 9ee16ea7922a1777e95cd5ecdf577c6d5935bbd2 (diff) | |
download | meta-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')
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 @@ | |||
1 | From f28a381b58516018ca35cdf7b4e2879a5bcac6ad Mon Sep 17 00:00:00 2001 | ||
2 | From: Thomas Maddox <thomas.maddox@rackspace.com> | ||
3 | Date: Mon, 21 Oct 2013 15:55:49 +0000 | ||
4 | Subject: [PATCH 1/2] Fix for get_resources with postgresql | ||
5 | |||
6 | Add max_ts and min_ts to GROUP BY in sub-query, since they need to be aggregated to SELECT them. | ||
7 | |||
8 | Closes-Bug: #1241526 | ||
9 | Change-Id: Ifdd2bc661b5da31bd40d1c3fa1fc442d7417399f | ||
10 | (cherry picked from commit 0a98159bc9c727a89bd8c15347ac380a21acaa59) | ||
11 | |||
12 | Signed-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 | |||
17 | diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py | ||
18 | index 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 | -- | ||
35 | 1.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 @@ | |||
1 | From 35488d1099c634d88d7e6c262eb9a6636ee2d7d8 Mon Sep 17 00:00:00 2001 | ||
2 | From: Gordon Chung <chungg@ca.ibm.com> | ||
3 | Date: Wed, 2 Oct 2013 15:45:26 -0400 | ||
4 | Subject: [PATCH 2/2] enable sql metadata query | ||
5 | |||
6 | explode metadata key/values to their own tables/rows (based on type). | ||
7 | build a key string using dot notation similar to other nosql db | ||
8 | and filter based on that. | ||
9 | |||
10 | Blueprint: sqlalchemy-metadata-query | ||
11 | Related-Bug: #1093625 | ||
12 | |||
13 | Change-Id: I2076e67b79448f98124a57b62b5bfed7aa8ae2ad | ||
14 | (cherry picked from commit 1570462507eae1478123de25dbadc64b09c82af3) | ||
15 | |||
16 | Signed-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 | |||
28 | diff --git a/ceilometer/storage/impl_sqlalchemy.py b/ceilometer/storage/impl_sqlalchemy.py | ||
29 | index 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, | ||
191 | diff --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 | ||
192 | new file mode 100644 | ||
193 | index 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() | ||
275 | diff --git a/ceilometer/storage/sqlalchemy/models.py b/ceilometer/storage/sqlalchemy/models.py | ||
276 | index 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 | |||
334 | diff --git a/ceilometer/utils.py b/ceilometer/utils.py | ||
335 | index 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 | ||
366 | diff --git a/doc/source/install/dbreco.rst b/doc/source/install/dbreco.rst | ||
367 | index 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 | ================== ============================= =================== ====== | ||
381 | diff --git a/tests/api/v2/test_list_meters_scenarios.py b/tests/api/v2/test_list_meters_scenarios.py | ||
382 | index 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', | ||
393 | diff --git a/tests/test_utils.py b/tests/test_utils.py | ||
394 | index 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 | -- | ||
418 | 1.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" | |||
10 | SRC_URI = "git://github.com/openstack/${SRCNAME}.git;branch=stable/havana \ | 10 | SRC_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 | ||
15 | SRCREV="4d15cc05e9d2ada01b90e1d3c15427608cc49f54" | 17 | SRCREV="4d15cc05e9d2ada01b90e1d3c15427608cc49f54" |