From 4a37980143b1ab53aea87cd853e025be7b0108a9 Mon Sep 17 00:00:00 2001 From: BenjamenMeyer Date: Wed, 13 Jul 2016 05:14:19 +0000 Subject: [PATCH] Enhancement: Session Listing and Information - Added support for GET /admin/{session-id} to get specific session data - Added support for GET /admin/ to list the running sessions - Refactored the stackinawsgi.session.service's URL extraction to make it more re-usable so as not to have to maintain the REGEX strings - Specified where global variables are *modified*; if just accessed, then nothing has changed - Added tracking of the sessions for created time, last accessed time, number of accesses via the HTTP interface, count of the various HTTP Response Status Codes - Updated existing tests accordingly - Added new tests where appropriate --- stackinawsgi/admin/admin.py | 112 +++++++++++- stackinawsgi/session/service.py | 8 +- stackinawsgi/session/session.py | 69 +++++++- stackinawsgi/test/test_admin_admin.py | 200 +++++++++++++++++++++- stackinawsgi/test/test_session_session.py | 60 ++++++- 5 files changed, 429 insertions(+), 20 deletions(-) diff --git a/stackinawsgi/admin/admin.py b/stackinawsgi/admin/admin.py index 6dad03d..5933dd5 100644 --- a/stackinawsgi/admin/admin.py +++ b/stackinawsgi/admin/admin.py @@ -1,11 +1,17 @@ """ Stack-In-A-WSGI: StackInAWsgiAdmin """ +import json import logging +import re from stackinabox.services.service import StackInABoxService from stackinawsgi.exceptions import InvalidSessionId +from stackinawsgi.session.service import ( + global_sessions, + session_regex +) logger = logging.getLogger(__name__) @@ -29,6 +35,12 @@ def __init__(self, session_manager, base_uri): self.manager = session_manager self.base_uri = base_uri + self.register( + StackInABoxService.GET, + re.compile('^{0}$'.format(session_regex)), + StackInAWsgiAdmin.get_session_info + ) + self.register( StackInABoxService.DELETE, '/', StackInAWsgiAdmin.remove_session ) @@ -39,7 +51,7 @@ def __init__(self, session_manager, base_uri): StackInABoxService.PUT, '/', StackInAWsgiAdmin.reset_session ) self.register( - StackInABoxService.GET, '/', StackInAWsgiAdmin.get_session_info + StackInABoxService.GET, '/', StackInAWsgiAdmin.get_sessions ) @property @@ -84,6 +96,30 @@ def helper_get_session_id(self, headers): logger.debug('Found Session Id: {0}'.format(session_id)) return session_id + def helper_get_session_id_from_uri(self, uri): + """ + Helper to retrieve the Session-ID FROM a URI + + :param text_type uri: complete URI + :returns: text_type with the session-id + """ + matcher = re.compile(session_regex) + try: + matched_groups = matcher.match(uri) + session_id = matched_groups.group(0)[1:] + logger.debug( + 'Helper Get Session From URI - URI: "{0}", ' + 'Session ID: "{1}"'.format( + uri, + session_id + ) + ) + + except Exception: + logger.exception('Failed to find session-id') + session_id = None + return session_id + def helper_get_uri(self, session_id): """ Helper to build the session URL @@ -214,10 +250,78 @@ def get_session_info(self, request, uri, headers): :returns: tuple for StackInABox HTTP Response HTTP Request: - GET /admin/ + GET /admin/{X-Session-ID} X-Session-ID: (Required) Session-ID to reset HTTP Responses: - 500 - Not Implemented + 200 - Session Data in JSON format + """ + requested_session_id = self.helper_get_session_id_from_uri( + uri + ) + + session_info = { + 'session_valid': requested_session_id in global_sessions, + 'created-time': None, + 'accessed-time': None, + 'accessed-count': 0, + 'http-status': {} + } + + if session_info['session_valid']: + session = global_sessions[requested_session_id] + session_info['created-time'] = session.created_at.isoformat() + session_info['accessed-time'] = ( + session.last_accessed_at.isoformat() + ) + session_info['accessed-count'] = session.access_count + session_info['http-status'] = session.status_tracker + + data = { + 'base_url': self.base_uri, + 'services': { + svc().name: svc.__name__ + for svc in self.manager.services + }, + 'trackers': { + 'created-time': session_info['created-time'], + 'accessed': { + 'time': session_info['accessed-time'], + 'count': session_info['accessed-count'] + }, + 'status': session_info['http-status'] + }, + 'session_valid': session_info['session_valid'] + } + + return (200, headers, json.dumps(data)) + + def get_sessions(self, request, uri, headers): + """ + Get Session List - TBD + + :param :obj:`Request` request: object containing the HTTP Request + :param text_type uri: the URI for the request per StackInABox + :param dict headers: case insensitive header dictionary + + :returns: tuple for StackInABox HTTP Response + + HTTP Request: + GET /admin/ + + HTTP Responses: + 200 - Session List in JSON format """ - return (500, headers, 'Not Implemented') + data = { + 'base_url': self.base_uri, + 'services': { + svc().name: svc.__name__ + for svc in self.manager.services + }, + 'sessions': [ + requested_session_id + for requested_session_id in global_sessions + ] + } + + return (200, headers, json.dumps(data)) diff --git a/stackinawsgi/session/service.py b/stackinawsgi/session/service.py index b481f24..04cfafb 100644 --- a/stackinawsgi/session/service.py +++ b/stackinawsgi/session/service.py @@ -21,6 +21,8 @@ # must be able to be pickled, which we can't guarantee. So # we're stuck with threading. global_sessions = dict() +session_regex = '^\/([\w-]+)' +session_regex_instance = '{0}\/.*'.format(session_regex) logger = logging.getLogger(__name__) @@ -66,7 +68,7 @@ def extract_session_id(uri): uri ) ) - matcher = re.compile('^\/([\w-]+)\/.*') + matcher = re.compile(session_regex_instance) matches = matcher.match(uri) if matches: @@ -128,6 +130,8 @@ def create_session(self, session_id=None): :returns: text_type with the session id """ + global global_sessions + logger.debug( 'Requesting creation of session. Optional Session Id: {0}'.format( session_id @@ -199,6 +203,8 @@ def remove_session(self, session_id): :raises: InvalidSessionId if session id is not found """ + global global_sessions + if session_id in global_sessions: del global_sessions[session_id] else: diff --git a/stackinawsgi/session/session.py b/stackinawsgi/session/session.py index 68421de..dfb3483 100644 --- a/stackinawsgi/session/session.py +++ b/stackinawsgi/session/session.py @@ -3,6 +3,7 @@ """ from __future__ import absolute_import +import datetime import logging from threading import Lock @@ -58,6 +59,52 @@ def __init__(self, session_id, services): self.stack = StackInABox() self.stack.base_url = self.session_id self.init_services() + self.created_at = datetime.datetime.utcnow() + self._last_accessed_time = self.created_at + self._access_count = 0 + self._http_status_dict = {} + + def _update_trackers(self): + """ + Update the session trackers + """ + self._access_count = self._access_count + 1 + self._last_accessed_time = datetime.datetime.utcnow() + + def _track_result(self, result): + """ + Track the results from StackInABox + """ + status, _, _ = result + if status not in self._http_status_dict: + self._http_status_dict[status] = 0 + + self._http_status_dict[status] = ( + self._http_status_dict[status] + 1 + ) + + return result + + @property + def last_accessed_at(self): + """ + Return the time the session was last accessed + """ + return self._last_accessed_time + + @property + def access_count(self): + """ + Return the number of times the session has been called + """ + return self._access_count + + @property + def status_tracker(self): + """ + Return the current copy of HTTP Status Code Trackers + """ + return self._http_status_dict def init_services(self): """ @@ -72,6 +119,7 @@ def init_services(self): ) ) self.stack.register(svc) + self._http_status_dict = {} @property def base_url(self): @@ -104,6 +152,7 @@ def reset(self): Reset the StackInABox instance to the initial state by resetting the instance then re-registering all the services. """ + self._update_trackers() logger.debug( 'Session {0}: Waiting for lock'.format( self.session_id @@ -123,6 +172,7 @@ def call(self, *args, **kwargs): """ Wrapper to same in the StackInABox instance """ + self._update_trackers() logger.debug( 'Session {0}: Waiting for lock'.format( self.session_id @@ -135,12 +185,15 @@ def call(self, *args, **kwargs): ) ) - return self.stack.call(*args, **kwargs) + return self._track_result( + self.stack.call(*args, **kwargs) + ) def try_handle_route(self, *args, **kwargs): """ Wrapper to same in the StackInABox instance """ + self._update_trackers() logger.debug( 'Session {0}: Waiting for lock'.format( self.session_id @@ -153,12 +206,15 @@ def try_handle_route(self, *args, **kwargs): ) ) - return self.stack.try_handle_route(*args, **kwargs) + return self._track_result( + self.stack.try_handle_route(*args, **kwargs) + ) def request(self, *args, **kwargs): """ Wrapper to same in the StackInABox instance """ + self._update_trackers() logger.debug( 'Session {0}: Waiting for lock'.format( self.session_id @@ -171,12 +227,15 @@ def request(self, *args, **kwargs): ) ) - return self.stack.request(*args, **kwargs) + return self._track_result( + self.stack.request(*args, **kwargs) + ) def sub_request(self, *args, **kwargs): """ Pass-thru to the StackInABox instance's sub_request """ + self._update_trackers() logger.debug( 'Session {0}: Waiting for lock'.format( self.session_id @@ -189,4 +248,6 @@ def sub_request(self, *args, **kwargs): ) ) - return self.stack.sub_request(*args, **kwargs) + return self._track_result( + self.stack.sub_request(*args, **kwargs) + ) diff --git a/stackinawsgi/test/test_admin_admin.py b/stackinawsgi/test/test_admin_admin.py index 91e5f24..9e73b10 100644 --- a/stackinawsgi/test/test_admin_admin.py +++ b/stackinawsgi/test/test_admin_admin.py @@ -1,8 +1,12 @@ """ Stack-In-A-WSGI: stackinawsgi.admin.admin.StackInAWsgiSessionManager """ +import datetime +import json import unittest +import ddt + from stackinabox.services.service import StackInABoxService from stackinabox.services.hello import HelloService @@ -16,6 +20,7 @@ from stackinawsgi.test.helpers import make_environment +@ddt.ddt class TestSessionManager(unittest.TestCase): """ Test the interaction of StackInAWSGI's Session Manager @@ -68,6 +73,8 @@ def test_property_base_uri_start_with_slash(self): def test_property_base_uri_ends_with_slash(self): """ + test the base uri property to ensure the trailing slash + is removed """ base_uri = 'hello/' admin = StackInAWsgiAdmin(self.manager, base_uri) @@ -333,6 +340,54 @@ def test_session_reset_invalid_session_id(self): # validate response self.assertEqual(response.status, 404) + @ddt.data(0, 1, 2, 3, 5, 8, 13) + def test_get_sessions(self, session_count): + """ + test get sessions + """ + admin = StackInAWsgiAdmin(self.manager, self.base_uri) + + uri = u'/' + environment = make_environment( + self, + method='GET', + path=uri[1:], + headers={} + ) + request = Request(environment) + + for _ in range(session_count): + admin.manager.create_session() + + response = Response() + result = admin.get_sessions( + request, + uri, + request.headers + ) + response.from_stackinabox( + result[0], + result[1], + result[2] + ) + # validate response + self.assertEqual(response.status, 200) + + response_body = response.body + session_data = json.loads(response_body) + + self.assertIn('base_url', session_data) + self.assertEqual(session_data['base_url'], self.base_uri) + + self.assertIn('services', session_data) + self.assertEqual(len(session_data['services']), 1) + + self.assertIn('hello', session_data['services']) + self.assertEqual(session_data['services']['hello'], 'HelloService') + + self.assertIn('sessions', session_data) + self.assertEqual(len(session_data['sessions']), session_count) + def test_get_session_info(self): """ test resetting a session with an invalid session id @@ -340,10 +395,10 @@ def test_get_session_info(self): admin = StackInAWsgiAdmin(self.manager, self.base_uri) session_id = 'my-session-id' - uri = u'/' + uri = u'/{0}'.format(session_id) environment = make_environment( self, - method='PUT', + method='GET', path=uri[1:], headers={ 'x-session-id': session_id @@ -352,8 +407,90 @@ def test_get_session_info(self): request = Request(environment) self.assertIn('x-session-id', request.headers) self.assertEqual(session_id, request.headers['x-session-id']) + + response_created = Response() + result_create = admin.create_session( + request, + uri, + request.headers + ) + response_created.from_stackinabox( + result_create[0], + result_create[1], + result_create[2] + ) + self.assertEqual(response_created.status, 201) + response = Response() + result = admin.get_session_info( + request, + uri, + request.headers + ) + response.from_stackinabox( + result[0], + result[1], + result[2] + ) + # validate response + self.assertEqual(response.status, 200) + + response_body = response.body + session_data = json.loads(response_body) + + self.assertIn('base_url', session_data) + self.assertEqual(session_data['base_url'], self.base_uri) + self.assertIn('session_valid', session_data) + self.assertTrue(session_data['session_valid']) + + self.assertIn('services', session_data) + self.assertEqual(len(session_data['services']), 1) + + self.assertIn('hello', session_data['services']) + self.assertEqual(session_data['services']['hello'], 'HelloService') + + self.assertIn('trackers', session_data) + self.assertEqual(len(session_data['trackers']), 3) + + self.assertIn('created-time', session_data['trackers']) + self.assertIsNotNone(session_data['trackers']['created-time']) + created_time = datetime.datetime.strptime( + session_data['trackers']['created-time'], + "%Y-%m-%dT%H:%M:%S.%f" + ) + + self.assertIn('accessed', session_data['trackers']) + self.assertEqual(len(session_data['trackers']['accessed']), 2) + self.assertIn('time', session_data['trackers']['accessed']) + self.assertIsNotNone(session_data['trackers']['accessed']['time']) + accessed_time = datetime.datetime.strptime( + session_data['trackers']['accessed']['time'], + "%Y-%m-%dT%H:%M:%S.%f" + ) + self.assertEqual(created_time, accessed_time) + self.assertIn('count', session_data['trackers']['accessed']) + + self.assertIn('status', session_data['trackers']) + self.assertEqual(len(session_data['trackers']['status']), 0) + + def test_get_session_info_invalid_session(self): + """ + test resetting a session with an invalid session id + """ + admin = StackInAWsgiAdmin(self.manager, self.base_uri) + + session_id = 'my-session-id' + uri = u'/{0}'.format(session_id) + + environment = make_environment( + self, + method='PUT', + path=uri[1:], + ) + request = Request(environment) + + response = Response() result = admin.get_session_info( request, uri, @@ -365,4 +502,61 @@ def test_get_session_info(self): result[2] ) # validate response - self.assertEqual(response.status, 500) + self.assertEqual(response.status, 200) + + response_body = response.body + session_data = json.loads(response_body) + + self.assertIn('base_url', session_data) + self.assertEqual(session_data['base_url'], self.base_uri) + + self.assertIn('session_valid', session_data) + self.assertFalse(session_data['session_valid']) + + self.assertIn('services', session_data) + self.assertEqual(len(session_data['services']), 1) + + self.assertIn('hello', session_data['services']) + self.assertEqual(session_data['services']['hello'], 'HelloService') + + self.assertIn('trackers', session_data) + self.assertEqual(len(session_data['trackers']), 3) + + self.assertIn('created-time', session_data['trackers']) + self.assertIsNone(session_data['trackers']['created-time']) + + self.assertIn('accessed', session_data['trackers']) + self.assertEqual(len(session_data['trackers']['accessed']), 2) + self.assertIn('time', session_data['trackers']['accessed']) + self.assertIsNone(session_data['trackers']['accessed']['time']) + self.assertIn('count', session_data['trackers']['accessed']) + + self.assertIn('status', session_data['trackers']) + self.assertEqual(len(session_data['trackers']['status']), 0) + + def test_extract_session_from_uri(self): + """ + test extracting a session from the URI - positive test + """ + admin = StackInAWsgiAdmin(self.manager, self.base_uri) + + session_id = 'my-session-id' + uri = u'/{0}'.format(session_id) + + extracted_session_id = admin.helper_get_session_id_from_uri( + uri + ) + self.assertEqual(session_id, extracted_session_id) + + def test_extract_session_from_uri_invalid(self): + """ + test extracting a session from the URI - negative test + """ + admin = StackInAWsgiAdmin(self.manager, self.base_uri) + + uri = u'/' + + extracted_session_id = admin.helper_get_session_id_from_uri( + uri + ) + self.assertIsNone(extracted_session_id) diff --git a/stackinawsgi/test/test_session_session.py b/stackinawsgi/test/test_session_session.py index b59c1d6..c7045f0 100644 --- a/stackinawsgi/test/test_session_session.py +++ b/stackinawsgi/test/test_session_session.py @@ -70,6 +70,9 @@ def test_construction(self): self.assertTrue(isinstance(session.stack, StackInABox)) self.assertEqual(session.session_id, session.stack.base_url) self.assertEqual(len(self.services), len(session.stack.services)) + self.assertEqual(0, session.access_count) + self.assertEqual(session.created_at, session.last_accessed_at) + self.assertEqual(0, len(session.status_tracker)) tuple_services = tuple(self.services) for _, v in session.stack.services.items(): _, svc = v @@ -80,6 +83,8 @@ def test_base_url(self): Test Base URL """ def validate(s, b): + self.assertEqual(0, s.access_count) + self.assertEqual(s.created_at, s.last_accessed_at) self.assertEqual( b, s.base_url @@ -108,11 +113,18 @@ def get_service_instance_info(s): return list_ids session = Session(self.session_id, self.services) + self.assertEqual(0, session.access_count) + self.assertEqual(session.created_at, session.last_accessed_at) + self.assertEqual(0, len(session.status_tracker)) original_session_ids = get_service_instance_info(session) session.reset() + self.assertEqual(1, session.access_count) + self.assertLess(session.created_at, session.last_accessed_at) + self.assertEqual(0, len(session.status_tracker)) + new_session_ids = get_service_instance_info(session) for k, v in original_session_ids.items(): @@ -123,74 +135,106 @@ def test_call(self): """ test calling into the session """ + result = (200, {}, "we're all good") mock_session_stack_call = mock.Mock() - mock_session_stack_call.return_value = True + mock_session_stack_call.return_value = result session = Session(self.session_id, self.services) session.stack.call = mock_session_stack_call + self.assertEqual(0, session.access_count) + self.assertEqual(session.created_at, session.last_accessed_at) + self.assertEqual(0, len(session.status_tracker)) self.assertEqual(mock_session_stack_call.call_count, 0) - self.assertTrue(session.call('hello', 'world')) + self.assertEqual(session.call('hello', 'world'), result) self.assertTrue(mock_session_stack_call.called) self.assertEqual(mock_session_stack_call.call_count, 1) self.assertEqual( mock_session_stack_call.call_args, (('hello', 'world'),) ) + self.assertEqual(1, session.access_count) + self.assertLess(session.created_at, session.last_accessed_at) + self.assertIn(result[0], session.status_tracker) + self.assertEqual(1, session.status_tracker[result[0]]) def test_try_handle_route(self): """ test calling the session request handler """ + result = (200, {}, "we're all good") mock_session_try_handle_route = mock.Mock() - mock_session_try_handle_route.return_value = True + mock_session_try_handle_route.return_value = result session = Session(self.session_id, self.services) session.stack.try_handle_route = mock_session_try_handle_route + self.assertEqual(0, session.access_count) + self.assertEqual(session.created_at, session.last_accessed_at) + self.assertEqual(0, len(session.status_tracker)) self.assertEqual(mock_session_try_handle_route.call_count, 0) - self.assertTrue(session.try_handle_route('hello', 'world')) + self.assertEqual(session.try_handle_route('hello', 'world'), result) self.assertTrue(mock_session_try_handle_route.called) self.assertEqual(mock_session_try_handle_route.call_count, 1) self.assertEqual( mock_session_try_handle_route.call_args, (('hello', 'world'),) ) + self.assertEqual(1, session.access_count) + self.assertLess(session.created_at, session.last_accessed_at) + self.assertIn(result[0], session.status_tracker) + self.assertEqual(1, session.status_tracker[result[0]]) def test_request(self): """ test calling the session request handler """ + result = (200, {}, "we're all good") mock_session_request = mock.Mock() - mock_session_request.return_value = True + mock_session_request.return_value = result session = Session(self.session_id, self.services) session.stack.request = mock_session_request + self.assertEqual(0, session.access_count) + self.assertEqual(session.created_at, session.last_accessed_at) + self.assertEqual(0, len(session.status_tracker)) self.assertEqual(mock_session_request.call_count, 0) - self.assertTrue(session.request('hello', 'world')) + self.assertEqual(session.request('hello', 'world'), result) self.assertTrue(mock_session_request.called) self.assertEqual(mock_session_request.call_count, 1) self.assertEqual( mock_session_request.call_args, (('hello', 'world'),) ) + self.assertEqual(1, session.access_count) + self.assertLess(session.created_at, session.last_accessed_at) + self.assertIn(result[0], session.status_tracker) + self.assertEqual(1, session.status_tracker[result[0]]) def test_sub_request(self): """ test calling the session sub_request handler """ + result = (200, {}, "we're all good") mock_session_sub_request = mock.Mock() - mock_session_sub_request.return_value = True + mock_session_sub_request.return_value = result session = Session(self.session_id, self.services) session.stack.sub_request = mock_session_sub_request + self.assertEqual(0, session.access_count) + self.assertEqual(session.created_at, session.last_accessed_at) + self.assertEqual(0, len(session.status_tracker)) self.assertEqual(mock_session_sub_request.call_count, 0) - self.assertTrue(session.sub_request('hello', 'world')) + self.assertEqual(session.sub_request('hello', 'world'), result) self.assertTrue(mock_session_sub_request.called) self.assertEqual(mock_session_sub_request.call_count, 1) self.assertEqual( mock_session_sub_request.call_args, (('hello', 'world'),) ) + self.assertEqual(1, session.access_count) + self.assertLess(session.created_at, session.last_accessed_at) + self.assertIn(result[0], session.status_tracker) + self.assertEqual(1, session.status_tracker[result[0]])