Make the v2 API date query parameters consistent

Use a DateRange object to specify the old start_timestamp,
end_timestamp, and search_offset parameters in a consistent
way for all of the v2 API calls.

This change addresses bug 1094337 and fixes problems with
running the v2 API tests against the most current version
of WSME, which expects all arguments to be listed on the
controller methods explicitly.

The changes to the test-requires files install the
spidermonkey component, needed for some of the tests
that were being skipped, causing this problem to go
unnoticed for a while.

Change-Id: I4d8677730f228701b0cb85e2ab4294958243e31e
Signed-off-by: Doug Hellmann <doug.hellmann@dreamhost.com>
This commit is contained in:
Doug Hellmann 2012-12-28 15:46:07 -05:00
parent b5e3d5d4cb
commit 2660cd7d56
13 changed files with 178 additions and 146 deletions

View File

@ -95,50 +95,6 @@ from ceilometer import storage
LOG = logging.getLogger(__name__)
def _get_query_timestamps(args={}):
"""Return any optional timestamp information in the request.
Determine the desired range, if any, from the GET arguments. Set
up the query range using the specified offset.
[query_start ... start_timestamp ... end_timestamp ... query_end]
Returns a dictionary containing:
query_start: First timestamp to use for query
start_timestamp: start_timestamp parameter from request
query_end: Final timestamp to use for query
end_timestamp: end_timestamp parameter from request
search_offset: search_offset parameter from request
"""
search_offset = int(args.get('search_offset', 0))
start_timestamp = args.get('start_timestamp')
if start_timestamp:
start_timestamp = timeutils.parse_isotime(start_timestamp)
start_timestamp = start_timestamp.replace(tzinfo=None)
query_start = (start_timestamp -
datetime.timedelta(minutes=search_offset))
else:
query_start = None
end_timestamp = args.get('end_timestamp')
if end_timestamp:
end_timestamp = timeutils.parse_isotime(end_timestamp)
end_timestamp = end_timestamp.replace(tzinfo=None)
query_end = end_timestamp + datetime.timedelta(minutes=search_offset)
else:
query_end = None
return {'query_start': query_start,
'query_end': query_end,
'start_timestamp': start_timestamp,
'end_timestamp': end_timestamp,
'search_offset': search_offset,
}
# FIXME(dhellmann): Change APIs that use this to return float?
class MeterVolume(Base):
volume = wsattr(float, mandatory=False)
@ -149,13 +105,59 @@ class MeterVolume(Base):
super(MeterVolume, self).__init__(volume=volume, **kw)
class DateRange(Base):
start = datetime.datetime
end = datetime.datetime
search_offset = int
def __init__(self, start=None, end=None, search_offset=0):
if start is not None:
start = start.replace(tzinfo=None)
if end is not None:
end = end.replace(tzinfo=None)
super(DateRange, self).__init__(start=start,
end=end,
search_offset=search_offset,
)
@property
def query_start(self):
"""The timestamp the query should use to start, including
the search offset.
"""
if self.start is None:
return None
return (self.start -
datetime.timedelta(minutes=self.search_offset))
@property
def query_end(self):
"""The timestamp the query should use to end, including
the search offset.
"""
if self.end is None:
return None
return (self.end +
datetime.timedelta(minutes=self.search_offset))
def to_dict(self):
return {'query_start': self.query_start,
'query_end': self.query_end,
'start_timestamp': self.start,
'end_timestamp': self.end,
'search_offset': self.search_offset,
}
class MeterVolumeController(object):
@wsme.pecan.wsexpose(MeterVolume)
def max(self):
@wsme.pecan.wsexpose(MeterVolume, DateRange)
def max(self, daterange=None):
"""Find the maximum volume for the matching meter events.
"""
q_ts = _get_query_timestamps(request.params)
if daterange is None:
daterange = DateRange()
q_ts = daterange.to_dict()
try:
meter = request.context['meter_id']
@ -193,11 +195,13 @@ class MeterVolumeController(object):
return MeterVolume(volume=value)
@wsme.pecan.wsexpose(MeterVolume)
def sum(self):
@wsme.pecan.wsexpose(MeterVolume, DateRange)
def sum(self, daterange=None):
"""Compute the total volume for the matching meter events.
"""
q_ts = _get_query_timestamps(request.params)
if daterange is None:
daterange = DateRange()
q_ts = daterange.to_dict()
try:
meter = request.context['meter_id']
@ -285,16 +289,17 @@ class MeterController(RestController):
request.context['meter_id'] = meter_id
self._id = meter_id
@wsme.pecan.wsexpose([Event])
def get_all(self):
@wsme.pecan.wsexpose([Event], DateRange)
def get_all(self, daterange=None):
"""Return all events for the meter.
"""
q_ts = _get_query_timestamps(request.params)
if daterange is None:
daterange = DateRange()
f = storage.EventFilter(
user=request.context.get('user_id'),
project=request.context.get('project_id'),
start=q_ts['query_start'],
end=q_ts['query_end'],
start=daterange.query_start,
end=daterange.query_end,
resource=request.context.get('resource_id'),
meter=self._id,
source=request.context.get('source_id'),
@ -303,24 +308,19 @@ class MeterController(RestController):
for e in request.storage_conn.get_raw_events(f)
]
# TODO(jd) replace str for timestamp by datetime?
@wsme.pecan.wsexpose(Duration, str, str, int)
def duration(self, start_timestamp=None, end_timestamp=None,
search_offset=0):
@wsme.pecan.wsexpose(Duration, DateRange)
def duration(self, daterange=None):
"""Computes the duration of the meter events in the time range given.
"""
q_ts = _get_query_timestamps(dict(start_timestamp=start_timestamp,
end_timestamp=end_timestamp,
search_offset=search_offset))
start_timestamp = q_ts['start_timestamp']
end_timestamp = q_ts['end_timestamp']
if daterange is None:
daterange = DateRange()
# Query the database for the interval of timestamps
# within the desired range.
f = storage.EventFilter(user=request.context.get('user_id'),
project=request.context.get('project_id'),
start=q_ts['query_start'],
end=q_ts['query_end'],
start=daterange.query_start,
end=daterange.query_end,
resource=request.context.get('resource_id'),
meter=self._id,
source=request.context.get('source_id'),
@ -330,12 +330,12 @@ class MeterController(RestController):
# "Clamp" the timestamps we return to the original time
# range, excluding the offset.
LOG.debug('start_timestamp %s, end_timestamp %s, min_ts %s, max_ts %s',
start_timestamp, end_timestamp, min_ts, max_ts)
if start_timestamp and min_ts and min_ts < start_timestamp:
min_ts = start_timestamp
daterange.start, daterange.end, min_ts, max_ts)
if daterange.start and min_ts and min_ts < daterange.start:
min_ts = daterange.start
LOG.debug('clamping min timestamp to range')
if end_timestamp and max_ts and max_ts > end_timestamp:
max_ts = end_timestamp
if daterange.end and max_ts and max_ts > daterange.end:
max_ts = daterange.end
LOG.debug('clamping max timestamp to range')
# If we got valid timestamps back, compute a duration in minutes.

View File

@ -108,6 +108,7 @@ class FunctionalTest(unittest.TestCase):
'logging': {
'loggers': {
'root': {'level': 'INFO', 'handlers': ['console']},
'wsme': {'level': 'INFO', 'handlers': ['console']},
'ceilometer': {'level': 'DEBUG',
'handlers': ['console'],
},
@ -154,11 +155,15 @@ class FunctionalTest(unittest.TestCase):
self.mox.VerifyAll()
set_config({}, overwrite=True)
def get_json(self, path, expect_errors=False, headers=None, **params):
def get_json(self, path, expect_errors=False, headers=None,
extra_params={}, **params):
full_path = self.PATH_PREFIX + path
print 'GET: %s %r' % (full_path, params)
all_params = {}
all_params.update(params)
all_params.update(extra_params)
print 'GET: %s %r' % (full_path, all_params)
response = self.app.get(full_path,
params=params,
params=all_params,
headers=headers,
expect_errors=expect_errors)
if not expect_errors:

View File

@ -44,7 +44,7 @@ class TestAPIACL(FunctionalTest):
def test_non_authenticated(self):
response = self.get_json('/sources', expect_errors=True)
self.assertEqual(response.status_code, 401)
self.assertEqual(response.status_int, 401)
def test_authenticated_wrong_role(self):
response = self.get_json('/sources',
@ -54,7 +54,7 @@ class TestAPIACL(FunctionalTest):
"X-Tenant-Name": "admin",
"X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb",
})
self.assertEqual(response.status_code, 401)
self.assertEqual(response.status_int, 401)
# FIXME(dhellmann): This test is not properly looking at the tenant
# info. We do not correctly detect the improper tenant. That's
@ -69,7 +69,7 @@ class TestAPIACL(FunctionalTest):
# "X-Tenant-Name": "achoo",
# "X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb",
# })
# self.assertEqual(response.status_code, 401)
# self.assertEqual(response.status_int, 401)
def test_authenticated(self):
response = self.get_json('/sources',
@ -79,4 +79,4 @@ class TestAPIACL(FunctionalTest):
"X-Tenant-Name": "admin",
"X-Tenant-Id": "bc23a9d531064583ace8f67dad60f6bb",
})
self.assertEqual(response.status_code, 200)
self.assertEqual(response.status_int, 200)

View File

@ -64,10 +64,12 @@ class TestComputeDurationByResource(FunctionalTest):
def _invoke_api(self):
return self.get_json(
'/resources/resource-id/meters/instance:m1.tiny/duration',
start_timestamp=self.start.isoformat(),
end_timestamp=self.end.isoformat(),
search_offset=10, # this value doesn't matter, db call is mocked
)
extra_params={
'daterange.start': self.start.isoformat(),
'daterange.end': self.end.isoformat(),
# this value doesn't matter, db call is mocked
'daterange.search_offset': 10,
})
def test_before_range(self):
self._set_interval(self.early1, self.early2)
@ -124,9 +126,11 @@ class TestComputeDurationByResource(FunctionalTest):
self._stub_interval_func(get_interval)
data = self.get_json(
'/resources/resource-id/meters/instance:m1.tiny/duration',
start_timestamp=self.late1.isoformat(),
search_offset=10, # this value doesn't matter, db call is mocked
)
extra_params={
'daterange.start': self.late1.isoformat(),
# this value doesn't matter, db call is mocked
'daterange.search_offset': 10,
})
self._assert_times_match(data['start_timestamp'], self.late1)
self._assert_times_match(data['end_timestamp'], self.late2)
@ -136,8 +140,10 @@ class TestComputeDurationByResource(FunctionalTest):
self._stub_interval_func(get_interval)
data = self.get_json(
'/resources/resource-id/meters/instance:m1.tiny/duration',
end_timestamp=self.early2.isoformat(),
search_offset=10, # this value doesn't matter, db call is mocked
)
extra_params={
'daterange.end': self.early2.isoformat(),
# this value doesn't matter, db call is mocked
'daterange.search_offset': 10,
})
self._assert_times_match(data['start_timestamp'], self.early1)
self._assert_times_match(data['end_timestamp'], self.early2)

View File

@ -24,10 +24,10 @@ import datetime
from ceilometer.api.controllers import v2 as api
class TimestampTest(unittest.TestCase):
class DateRangeTest(unittest.TestCase):
def test_get_query_timestamps_none_specified(self):
result = api._get_query_timestamps()
result = api.DateRange().to_dict()
expected = {'start_timestamp': None,
'end_timestamp': None,
'query_start': None,
@ -38,8 +38,8 @@ class TimestampTest(unittest.TestCase):
assert result == expected
def test_get_query_timestamps_start(self):
args = {'start_timestamp': '2012-09-20T12:13:14'}
result = api._get_query_timestamps(args)
d = datetime.datetime(2012, 9, 20, 12, 13, 14)
result = api.DateRange(start=d).to_dict()
expected = {
'start_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
'end_timestamp': None,
@ -51,8 +51,8 @@ class TimestampTest(unittest.TestCase):
assert result == expected
def test_get_query_timestamps_end(self):
args = {'end_timestamp': '2012-09-20T12:13:14'}
result = api._get_query_timestamps(args)
d = datetime.datetime(2012, 9, 20, 12, 13, 14)
result = api.DateRange(end=d).to_dict()
expected = {
'end_timestamp': datetime.datetime(2012, 9, 20, 12, 13, 14),
'start_timestamp': None,
@ -64,11 +64,11 @@ class TimestampTest(unittest.TestCase):
assert result == expected
def test_get_query_timestamps_with_offset(self):
args = {'start_timestamp': '2012-09-20T12:13:14',
'end_timestamp': '2012-09-20T13:24:25',
'search_offset': '20',
}
result = api._get_query_timestamps(args)
result = api.DateRange(
end=datetime.datetime(2012, 9, 20, 13, 24, 25),
start=datetime.datetime(2012, 9, 20, 12, 13, 14),
search_offset=20,
).to_dict()
expected = {
'query_end': datetime.datetime(2012, 9, 20, 13, 44, 25),
'query_start': datetime.datetime(2012, 9, 20, 11, 53, 14),

View File

@ -95,7 +95,7 @@ class TestListEvents(FunctionalTest):
def test_empty_source(self):
data = self.get_json('/sources/no-such-source/meters/instance',
expect_errors=True)
self.assertEquals(data.status_code, 404)
self.assertEquals(data.status_int, 404)
def test_by_source(self):
data = self.get_json('/sources/test_source/meters/instance')

View File

@ -40,7 +40,7 @@ class TestListSource(FunctionalTest):
'/sources/test_source_that_does_not_exist',
expect_errors=True)
print 'GOT:', ydata
self.assertEqual(ydata.status_code, 404)
self.assertEqual(ydata.status_int, 404)
self.assert_(
"No source test_source_that_does_not_exist" in
ydata.json['error_message']

View File

@ -64,32 +64,37 @@ class TestMaxProjectVolume(FunctionalTest):
self.assertEqual(data, expected)
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00'})
expected = {'volume': 7}
self.assertEqual(data, expected)
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T12:34:00'})
expected = {'volume': None}
self.assertEqual(data, expected)
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T11:30:00'})
expected = {'volume': 5}
self.assertEqual(data, expected)
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T09:54:00'})
expected = {'volume': None}
self.assertEqual(data, expected)
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00',
'daterange.end': '2012-09-25T11:32:00'})
expected = {'volume': 6}
self.assertEqual(data, expected)

View File

@ -63,32 +63,37 @@ class TestMaxResourceVolume(FunctionalTest):
assert data == expected
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00'})
expected = {'volume': 7}
assert data == expected
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T12:34:00'})
expected = {'volume': None}
assert data == expected
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T11:30:00'})
expected = {'volume': 5}
assert data == expected
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T09:54:00'})
expected = {'volume': None}
assert data == expected
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00',
'daterange.end': '2012-09-25T11:32:00'})
expected = {'volume': 6}
assert data == expected

View File

@ -63,32 +63,37 @@ class TestSumProjectVolume(FunctionalTest):
assert data == expected
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00'})
expected = {'volume': 6 + 7}
assert data == expected
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T12:34:00'})
expected = {'volume': None}
assert data == expected
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T11:30:00'})
expected = {'volume': 5}
assert data == expected
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T09:54:00'})
expected = {'volume': None}
assert data == expected
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00',
'daterange.end': '2012-09-25T11:32:00'})
expected = {'volume': 6}
assert data == expected

View File

@ -63,32 +63,37 @@ class TestSumResourceVolume(FunctionalTest):
assert data == expected
def test_start_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00'})
expected = {'volume': 6 + 7}
assert data == expected
def test_start_timestamp_after(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T12:34:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T12:34:00'})
expected = {'volume': None}
assert data == expected
def test_end_timestamp(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T11:30:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T11:30:00'})
expected = {'volume': 5}
assert data == expected
def test_end_timestamp_before(self):
data = self.get_json(self.PATH,
end_timestamp='2012-09-25T09:54:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.end': '2012-09-25T09:54:00'})
expected = {'volume': None}
assert data == expected
def test_start_end_timestamp(self):
data = self.get_json(self.PATH,
start_timestamp='2012-09-25T11:30:00',
end_timestamp='2012-09-25T11:32:00')
data = self.get_json(
self.PATH,
extra_params={'daterange.start': '2012-09-25T11:30:00',
'daterange.end': '2012-09-25T11:32:00'})
expected = {'volume': 6}
assert data == expected

View File

@ -26,7 +26,7 @@ pecan
swift
# Swift dep that is not necessary if we depend on swift>1.7.5
netifaces
# Docs Requirements
sphinx
docutils==0.9.1 # for bug 1091333, remove after sphinx >1.1.3 is released.
python-spidermonkey

View File

@ -25,3 +25,4 @@ pecan
http://tarballs.openstack.org/swift/swift-stable-folsom.tar.gz
# Swift dep that is not necessary if we depend on swift>1.7.5
netifaces
python-spidermonkey