srikanth | 116e6e8 | 2014-08-19 07:22:37 -0700 | [diff] [blame] | 1 | # |
| 2 | # Copyright (c) 2013 Big Switch Networks, Inc. |
| 3 | # |
| 4 | # Licensed under the Eclipse Public License, Version 1.0 (the |
| 5 | # "License"); you may not use this file except in compliance with the |
| 6 | # License. You may obtain a copy of the License at |
| 7 | # |
| 8 | # http://www.eclipse.org/legal/epl-v10.html |
| 9 | # |
| 10 | # Unless required by applicable law or agreed to in writing, software |
| 11 | # distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
| 13 | # implied. See the License for the specific language governing |
| 14 | # permissions and limitations under the License. |
| 15 | # |
| 16 | |
| 17 | from cassandra.ttypes import * |
| 18 | from django.conf import settings |
| 19 | from django.http import HttpResponse |
| 20 | from django.utils import simplejson |
| 21 | from functools import wraps |
| 22 | import time |
| 23 | from .data import StatsException, StatsInvalidStatsDataException, \ |
| 24 | StatsInvalidStatsTypeException, \ |
| 25 | get_stats_db_connection, init_stats_db_connection, \ |
| 26 | get_stats_metadata, get_stats_type_index, \ |
| 27 | get_stats_target_types, get_stats_targets, delete_stats_data, \ |
| 28 | get_stats_data, get_latest_stat_data, put_stats_data, \ |
| 29 | get_closest_sample_interval, get_closest_window_interval, \ |
| 30 | get_log_event_data, put_log_event_data, delete_log_event_data, \ |
| 31 | VALUE_DATA_FORMAT |
| 32 | |
| 33 | from sdncon.rest.views import RestException, \ |
| 34 | RestInvalidPutDataException, RestMissingRequiredQueryParamException,\ |
| 35 | RestInvalidMethodException, RestDatabaseConnectionException,\ |
| 36 | RestInternalException, RestResourceNotFoundException, \ |
| 37 | safe_rest_view, JSON_CONTENT_TYPE, get_successful_response |
| 38 | from sdncon.controller.config import get_local_controller_id |
| 39 | |
| 40 | |
| 41 | class RestStatsException(RestException): |
| 42 | def __init__(self, stats_exception): |
| 43 | super(RestStatsException,self).__init__('Error accessing stats: ' + str(stats_exception)) |
| 44 | |
| 45 | class RestStatsInvalidTimeDurationUnitsException(RestException): |
| 46 | def __init__(self, units): |
| 47 | super(RestStatsInvalidTimeDurationUnitsException,self).__init__('Invalid time duration units: ' + str(units)) |
| 48 | |
| 49 | |
| 50 | class RestStatsInvalidTimeRangeException(RestException): |
| 51 | def __init__(self): |
| 52 | super(RestStatsInvalidTimeRangeException,self).__init__('Invalid time range specified in stats REST API. ' |
| 53 | '2 out of 3 of start-time, end-time, and duration params must be specified.') |
| 54 | |
| 55 | |
| 56 | @safe_rest_view |
| 57 | def safe_stats_rest_view(func, *args, **kwargs): |
| 58 | try: |
| 59 | request = args[0] |
| 60 | response = func(*args, **kwargs) |
| 61 | except RestException: |
| 62 | raise |
| 63 | except StatsInvalidStatsDataException: |
| 64 | raise RestInvalidPutDataException() |
| 65 | except StatsInvalidStatsTypeException: |
| 66 | raise RestResourceNotFoundException(request.path) |
| 67 | except StatsException, e: |
| 68 | raise RestStatsException(e) |
| 69 | except Exception, e: |
| 70 | raise RestInternalException(e) |
| 71 | return response |
| 72 | |
| 73 | |
| 74 | def safe_stats_view(func): |
| 75 | """ |
| 76 | This is a decorator that takes care of exception handling for the |
| 77 | stats views so that stats exceptions are converted to the appropriate |
| 78 | REST exception. |
| 79 | """ |
| 80 | @wraps(func) |
| 81 | def _func(*args, **kwargs): |
| 82 | response = safe_stats_rest_view(func, *args, **kwargs) |
| 83 | return response |
| 84 | |
| 85 | return _func |
| 86 | |
| 87 | |
| 88 | def init_db_connection(): |
| 89 | db_connection = get_stats_db_connection() |
| 90 | if not db_connection: |
| 91 | try: |
| 92 | stats_db_settings = settings.STATS_DATABASE |
| 93 | except Exception: |
| 94 | stats_db_settings = {} |
| 95 | |
| 96 | host = stats_db_settings.get('HOST', 'localhost') |
| 97 | port = stats_db_settings.get('PORT', 9160) |
| 98 | keyspace = stats_db_settings.get('NAME', 'sdnstats') |
| 99 | user = stats_db_settings.get('USER') |
| 100 | password = stats_db_settings.get('PASSWORD') |
| 101 | replication_factor = stats_db_settings.get('CASSANDRA_REPLICATION_FACTOR', 1) |
| 102 | column_family_def_default_settings = stats_db_settings.get('CASSANDRA_COLUMN_FAMILY_DEF_DEFAULT_SETTINGS', {}) |
| 103 | |
| 104 | init_stats_db_connection(host, port, keyspace, user, password, replication_factor, column_family_def_default_settings) |
| 105 | |
| 106 | db_connection = get_stats_db_connection() |
| 107 | assert(db_connection is not None) |
| 108 | |
| 109 | START_TIME_QUERY_PARAM = 'start-time' |
| 110 | END_TIME_QUERY_PARAM = 'end-time' |
| 111 | DURATION_QUERY_PARAM = 'duration' |
| 112 | SAMPLE_INTERVAL_QUERY_PARAM = 'sample-interval' |
| 113 | SAMPLE_COUNT_QUERY_PARAM = 'sample-count' |
| 114 | SAMPLE_WINDOW_QUERY_PARAM = 'sample-window' |
| 115 | DATA_FORMAT_QUERY_PARAM = 'data-format' |
| 116 | LIMIT_QUERY_PARAM = 'limit' |
| 117 | INCLUDE_PK_TAG_QUERY_PARAM = 'include-pk-tag' |
| 118 | |
| 119 | DEFAULT_SAMPLE_COUNT = 50 |
| 120 | |
| 121 | def convert_time_point(time_point): |
| 122 | |
| 123 | if time_point is None: |
| 124 | return None |
| 125 | |
| 126 | if time_point: |
| 127 | time_point = time_point.lower() |
| 128 | if time_point in ('now', 'current'): |
| 129 | time_point = int(time.time() * 1000) |
| 130 | else: |
| 131 | time_point = int(time_point) |
| 132 | |
| 133 | return time_point |
| 134 | |
| 135 | |
| 136 | UNIT_CONVERSIONS = ( |
| 137 | (('h', 'hour', 'hours'), 3600000), |
| 138 | (('d', 'day', 'days'), 86400000), |
| 139 | (('w', 'week', 'weeks'), 604800000), |
| 140 | (('m', 'min', 'mins', 'minute', 'minutes'), 60000), |
| 141 | (('s', 'sec', 'secs', 'second', 'seconds'), 1000), |
| 142 | (('ms', 'millisecond', 'milliseconds'), 1) |
| 143 | ) |
| 144 | |
| 145 | def convert_time_duration(duration): |
| 146 | |
| 147 | if duration is None: |
| 148 | return None |
| 149 | |
| 150 | value = "" |
| 151 | for c in duration: |
| 152 | if not c.isdigit(): |
| 153 | break |
| 154 | value += c |
| 155 | |
| 156 | units = duration[len(value):].lower() |
| 157 | value = int(value) |
| 158 | |
| 159 | if units: |
| 160 | converted_value = None |
| 161 | for conversion in UNIT_CONVERSIONS: |
| 162 | if units in conversion[0]: |
| 163 | converted_value = value * conversion[1] |
| 164 | break |
| 165 | if converted_value is None: |
| 166 | raise RestStatsInvalidTimeDurationUnitsException(units) |
| 167 | |
| 168 | value = converted_value |
| 169 | |
| 170 | return value |
| 171 | |
| 172 | |
| 173 | def get_time_range(start_time, end_time, duration): |
| 174 | |
| 175 | if not start_time and not end_time and not duration: |
| 176 | return (None, None) |
| 177 | |
| 178 | start_time = convert_time_point(start_time) |
| 179 | end_time = convert_time_point(end_time) |
| 180 | duration = convert_time_duration(duration) |
| 181 | |
| 182 | if start_time: |
| 183 | if not end_time and duration: |
| 184 | end_time = start_time + duration |
| 185 | elif end_time and duration: |
| 186 | start_time = end_time - duration |
| 187 | |
| 188 | if not start_time or not end_time: |
| 189 | raise RestStatsInvalidTimeRangeException() |
| 190 | |
| 191 | return (start_time, end_time) |
| 192 | |
| 193 | |
| 194 | def get_time_range_from_request(request): |
| 195 | start_time = request.GET.get(START_TIME_QUERY_PARAM) |
| 196 | end_time = request.GET.get(END_TIME_QUERY_PARAM) |
| 197 | duration = request.GET.get(DURATION_QUERY_PARAM) |
| 198 | |
| 199 | return get_time_range(start_time, end_time, duration) |
| 200 | |
| 201 | #def get_stats_time_range(request): |
| 202 | # start_time = request.GET.get(START_TIME_QUERY_PARAM) |
| 203 | # end_time = request.GET.get(END_TIME_QUERY_PARAM) |
| 204 | # |
| 205 | # if not start_time and not end_time: |
| 206 | # return None |
| 207 | # |
| 208 | # if not start_time: |
| 209 | # raise RestMissingRequiredQueryParamException(START_TIME_QUERY_PARAM) |
| 210 | # if not end_time: |
| 211 | # raise RestMissingRequiredQueryParamException(END_TIME_QUERY_PARAM) |
| 212 | # |
| 213 | # return (start_time, end_time) |
| 214 | |
| 215 | |
| 216 | @safe_stats_view |
| 217 | def do_get_stats(request, cluster, target_type, target_id, stats_type): |
| 218 | |
| 219 | # FIXME: Hack to handle the old hard-coded controller id value |
| 220 | if target_type == 'controller' and target_id == 'localhost': |
| 221 | target_id = get_local_controller_id() |
| 222 | |
| 223 | # Get the time range over which we're getting the stats |
| 224 | start_time, end_time = get_time_range_from_request(request) |
| 225 | |
| 226 | init_db_connection() |
| 227 | |
| 228 | if request.method == 'GET': |
| 229 | window = request.GET.get(SAMPLE_WINDOW_QUERY_PARAM, 0) |
| 230 | if window: |
| 231 | window = convert_time_duration(window) |
| 232 | if window != 0: |
| 233 | window = get_closest_window_interval(int(window)) |
| 234 | # FIXME: Error checking on window value |
| 235 | |
| 236 | data_format = request.GET.get(DATA_FORMAT_QUERY_PARAM, VALUE_DATA_FORMAT) |
| 237 | # FIXME: Error checking on data_format value |
| 238 | |
| 239 | limit = request.GET.get(LIMIT_QUERY_PARAM) |
| 240 | if limit: |
| 241 | limit = int(limit) |
| 242 | # FIXME: Error checking on limit value |
| 243 | |
| 244 | if start_time is not None and end_time is not None: |
| 245 | # FIXME: Error checking on start_time and end_time values |
| 246 | sample_interval = request.GET.get(SAMPLE_INTERVAL_QUERY_PARAM) |
| 247 | if not sample_interval: |
| 248 | # FIXME: Error checking on sample_period value |
| 249 | sample_count = request.GET.get(SAMPLE_COUNT_QUERY_PARAM, DEFAULT_SAMPLE_COUNT) |
| 250 | # FIXME: Error checking on sample_count value |
| 251 | |
| 252 | sample_interval = (end_time - start_time) / int(sample_count) |
| 253 | else: |
| 254 | sample_interval = convert_time_duration(sample_interval) |
| 255 | |
| 256 | if sample_interval != 0: |
| 257 | sample_interval = get_closest_sample_interval(sample_interval) |
| 258 | |
| 259 | stats_data = get_stats_data(cluster, target_type, target_id, |
| 260 | stats_type, start_time, end_time, sample_interval, window, data_format, limit) |
| 261 | else: |
| 262 | stats_data = get_latest_stat_data(cluster, target_type, target_id, stats_type, window, data_format) |
| 263 | |
| 264 | response_data = simplejson.dumps(stats_data) |
| 265 | response = HttpResponse(response_data, JSON_CONTENT_TYPE) |
| 266 | |
| 267 | elif request.method == 'DELETE': |
| 268 | delete_stats_data(cluster, target_type, target_id, stats_type, |
| 269 | start_time, end_time) |
| 270 | response = get_successful_response() |
| 271 | else: |
| 272 | raise RestInvalidMethodException() |
| 273 | |
| 274 | return response |
| 275 | |
| 276 | |
| 277 | @safe_stats_view |
| 278 | def do_get_stats_metadata(request, cluster, stats_type=None): |
| 279 | metadata = get_stats_metadata(cluster, stats_type) |
| 280 | response_data = simplejson.dumps(metadata) |
| 281 | return HttpResponse(response_data, JSON_CONTENT_TYPE) |
| 282 | |
| 283 | |
| 284 | @safe_stats_view |
| 285 | def do_get_stats_type_index(request, cluster, target_type, target_id, stats_type=None): |
| 286 | # FIXME: Hack to handle the old hard-coded controller id value |
| 287 | if target_type == 'controller' and target_id == 'localhost': |
| 288 | target_id = get_local_controller_id() |
| 289 | init_db_connection() |
| 290 | index_data = get_stats_type_index(cluster, target_type, target_id, stats_type) |
| 291 | response_data = simplejson.dumps(index_data) |
| 292 | return HttpResponse(response_data, JSON_CONTENT_TYPE) |
| 293 | |
| 294 | |
| 295 | @safe_stats_view |
| 296 | def do_get_stats_target_types(request, cluster): |
| 297 | init_db_connection() |
| 298 | target_type_data = get_stats_target_types(cluster) |
| 299 | response_data = simplejson.dumps(target_type_data) |
| 300 | return HttpResponse(response_data, JSON_CONTENT_TYPE) |
| 301 | |
| 302 | |
| 303 | @safe_stats_view |
| 304 | def do_get_stats_targets(request, cluster, target_type=None): |
| 305 | init_db_connection() |
| 306 | target_data = get_stats_targets(cluster, target_type) |
| 307 | response_data = simplejson.dumps(target_data) |
| 308 | return HttpResponse(response_data, JSON_CONTENT_TYPE) |
| 309 | |
| 310 | |
| 311 | @safe_stats_view |
| 312 | def do_put_stats(request, cluster): |
| 313 | if request.method != 'PUT': |
| 314 | raise RestInvalidMethodException() |
| 315 | |
| 316 | init_db_connection() |
| 317 | |
| 318 | stats_data = simplejson.loads(request.raw_post_data) |
| 319 | put_stats_data(cluster, stats_data) |
| 320 | |
| 321 | response = get_successful_response() |
| 322 | |
| 323 | return response |
| 324 | |
| 325 | |
| 326 | @safe_stats_view |
| 327 | def do_get_events(request, cluster, node_id): |
| 328 | # FIXME: Hack to handle the old hard-coded controller id value |
| 329 | if node_id == 'localhost': |
| 330 | node_id = get_local_controller_id() |
| 331 | |
| 332 | # Get the time range over which we're getting the events |
| 333 | start_time, end_time = get_time_range_from_request(request) |
| 334 | |
| 335 | init_db_connection() |
| 336 | |
| 337 | if request.method == 'GET': |
| 338 | include_pk_tag_param = request.GET.get(INCLUDE_PK_TAG_QUERY_PARAM, 'false') |
| 339 | include_pk_tag = include_pk_tag_param.lower() == 'true' |
| 340 | events_list = get_log_event_data(cluster, node_id, start_time, end_time, include_pk_tag) |
| 341 | response_data = simplejson.dumps(events_list) |
| 342 | response = HttpResponse(response_data, JSON_CONTENT_TYPE) |
| 343 | elif request.method == 'DELETE': |
| 344 | delete_log_event_data(cluster, node_id, start_time, end_time) |
| 345 | response = get_successful_response() |
| 346 | else: |
| 347 | raise RestInvalidMethodException() |
| 348 | |
| 349 | return response |
| 350 | |
| 351 | @safe_stats_view |
| 352 | def do_put_events(request, cluster): |
| 353 | if request.method != 'PUT': |
| 354 | raise RestInvalidMethodException() |
| 355 | |
| 356 | init_db_connection() |
| 357 | |
| 358 | events_data = simplejson.loads(request.raw_post_data) |
| 359 | put_log_event_data(cluster, events_data) |
| 360 | |
| 361 | response = get_successful_response() |
| 362 | |
| 363 | return response |
| 364 | |