diff --git a/src/zinolib/controllers/base.py b/src/zinolib/controllers/base.py index abaaafc..4ccbe74 100644 --- a/src/zinolib/controllers/base.py +++ b/src/zinolib/controllers/base.py @@ -21,6 +21,7 @@ class ManagerException(Exception): def __init__(self, session=None): self.session = session self.events = {} + self.removed_ids = set() def _get_event(self, event_or_id: EventOrId) -> Event: if isinstance(event_or_id, Event): @@ -29,6 +30,13 @@ def _get_event(self, event_or_id: EventOrId) -> Event: return self.events[event_or_id] raise ValueError("Unknown type") + def _get_event_id(self, event_or_id: EventOrId) -> int: + if isinstance(event_or_id, int): + return event_or_id + if isinstance(event_or_id, Event): + return event_or_id.id + raise ValueError("Unknown type") + def _set_event(self, event: Event): self.events[event.id] = event diff --git a/src/zinolib/controllers/zino1.py b/src/zinolib/controllers/zino1.py index 268bc3c..53057e5 100644 --- a/src/zinolib/controllers/zino1.py +++ b/src/zinolib/controllers/zino1.py @@ -29,6 +29,18 @@ This is a dictionary of event_id, event object pairs. +To get a set of removed event ids:: + + > event_manager.removed_ids + +For updates, either regularly use ``get_events()`` or utilize the UpdateHandler:: + + > updater = UpdateHandler(event_manager) + > updated = updater.poll() + +This updates ``event_manager.events`` and ``event_manager.removed_ids`` and +returns ``True`` on any change, falsey otherwise. + To get history for a specific event:: > history_list = event_manager.get_history_for_id(INT) @@ -54,8 +66,10 @@ from datetime import datetime, timezone from typing import Iterable, List, TypedDict, Optional, Set +import logging from .base import EventManager +from ..compat import StrEnum from ..event_types import EventType, Event, HistoryEntry, LogEntry, AdmState from ..ritz import ProtocolError, ritz, notifier @@ -78,12 +92,75 @@ DEFAULT_TIMEOUT = 30 +LOG = logging.getLogger(__name__) def convert_timestamp(timestamp: int) -> datetime: return datetime.fromtimestamp(timestamp, timezone.utc) +class UpdateHandler: + class UpdateType(StrEnum): + STATE = "state" + ATTR = "attr" + HISTORY = "history" + LOG = "log" + SCAVENGED = "scavenged" + + def __init__(self, manager, autoremove=False): + self.manager = manager + self.events = manager.events + self.autoremove = autoremove + + def poll(self): + update = self.manager.session.push.poll() + if not update: + return False + return self.handle(update) + + def update(self, event_id: int): + event = self.manager.get_updated_event_for_id(event_id) + self.manager._set_event(event) + LOG.debug("Updated event #%i", event_id) + + def remove(self, event_id: int): + self.manager.remove_event(event_id) + LOG.debug("Removed event #%i", event_id) + + def handle(self, update): + if update.id not in self.events and update.type != self.UpdateType.STATE: + # new event that still don't have a state + return None + if update.type in tuple(self.UpdateType): + method = getattr(self, f"cmd_{update.type}") + return method(update) + return self.fallback(update) + + def cmd_state(self, update): + states = update.info.split(" ") + if states[1] == "closed" and self.autoremove: + LOG.debug('Autoremoving "%s"', update.id) + self.remove(update.id) + else: + self.update(update.id) + return True + + def cmd_attr(self, update): + self.update(update.id) + return True + + cmd_history = cmd_attr + cmd_log = cmd_attr + + def cmd_scavenged(self, update): + self.remove(update.id) + return True + + def fallback(self, update): + LOG.warning('Unknown update type: "%s" for id %s' % (update.type, update.id)) + return False + + class SessionAdapter: class _Session: @@ -126,6 +203,7 @@ def authenticate(session, username=None, password=None): def close_session(session): session.push._sock.close() session.request.close() + return None class EventAdapter: @@ -340,7 +418,7 @@ def clear_flapping(self, event: EventType): Usage: c = ritz_session.case(123) - c.clear_clapping() + c.clear_flapping() """ if event.type == Event.Type.PortState: return self.session.request.clear_flapping(event.router, event.ifindex) diff --git a/tests/test_zinolib_controllers_zino1.py b/tests/test_zinolib_controllers_zino1.py index e8a1220..d4914cd 100644 --- a/tests/test_zinolib_controllers_zino1.py +++ b/tests/test_zinolib_controllers_zino1.py @@ -2,7 +2,8 @@ from datetime import datetime, timedelta, timezone from zinolib.event_types import AdmState, Event, HistoryEntry, LogEntry -from zinolib.controllers.zino1 import EventAdapter, HistoryAdapter, LogAdapter, SessionAdapter, Zino1EventManager +from zinolib.controllers.zino1 import EventAdapter, HistoryAdapter, LogAdapter, SessionAdapter, Zino1EventManager, UpdateHandler +from zinolib.ritz import NotifierResponse raw_event_id = 139110 raw_attrlist = [ @@ -42,7 +43,7 @@ class FakeEventAdapter: @staticmethod def get_attrlist(request, event_id: int): - return raw_attrlist + return raw_attrlist.copy() @classmethod def attrlist_to_attrdict(cls, attrlist): @@ -60,13 +61,13 @@ def get_event_ids(request): class FakeHistoryAdapter(HistoryAdapter): @staticmethod def get_history(request, event_id: int): - return raw_history + return raw_history.copy() class FakeLogAdapter(LogAdapter): @staticmethod def get_log(request, event_id: int): - return raw_log + return raw_log.copy() class FakeSessionAdapter(SessionAdapter): @@ -144,3 +145,102 @@ def test_get_log_for_id(self): log='some other log message') ] self.assertEqual(log_list, expected_log_list) + + +class UpdateHandlerTest(unittest.TestCase): + + def init_manager(self): + zino1 = FakeZino1EventManager.configure(None) + return zino1 + + def test_cmd_scavenged(self): + zino1 = self.init_manager() + zino1.get_events() + self.assertIn(raw_event_id, zino1.events) + self.assertNotIn(raw_event_id, zino1.removed_ids) + updates = UpdateHandler(zino1) + update = NotifierResponse(raw_event_id, "","") + ok = updates.cmd_scavenged(update) + self.assertTrue(ok) + self.assertNotIn(raw_event_id, zino1.events) + self.assertIn(raw_event_id, zino1.removed_ids) + + def test_cmd_attr(self): + zino1 = self.init_manager() + zino1.get_events() + old_events = zino1.events.copy() + old_events[raw_event_id].priority = 500 + updates = UpdateHandler(zino1) + update = NotifierResponse(raw_event_id, "","") + ok = updates.cmd_attr(update) + self.assertTrue(ok) + self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority) + + def test_cmd_state_is_closed_and_autoremove_is_on(self): + zino1 = self.init_manager() + zino1.get_events() + self.assertNotIn(raw_event_id, zino1.removed_ids) + self.assertIn(raw_event_id, zino1.events) + updates = UpdateHandler(zino1, autoremove=True) + update = NotifierResponse(raw_event_id, "", "X closed") + ok = updates.cmd_state(update) + self.assertTrue(ok) + self.assertIn(raw_event_id, zino1.removed_ids) + self.assertNotIn(raw_event_id, zino1.events) + + def test_cmd_state_is_closed_and_autoremove_is_off(self): + zino1 = self.init_manager() + zino1.get_events() + old_events = zino1.events.copy() + old_events[raw_event_id].priority = 500 + updates = UpdateHandler(zino1, autoremove=False) + update = NotifierResponse(raw_event_id, "","X closed") + ok = updates.cmd_state(update) + self.assertTrue(ok) + self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority) + + def test_cmd_state_is_not_closed(self): + zino1 = self.init_manager() + zino1.get_events() + old_events = zino1.events.copy() + old_events[raw_event_id].priority = 500 + updates = UpdateHandler(zino1, autoremove=False) + update = NotifierResponse(raw_event_id, "","x butterfly") + ok = updates.cmd_state(update) + self.assertTrue(ok) + self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority) + + def test_fallback(self): + zino1 = self.init_manager() + updates = UpdateHandler(zino1) + update = NotifierResponse(raw_event_id, "", "") + with self.assertLogs('zinolib.controllers.zino1', level='WARNING') as cm: + self.assertFalse(updates.fallback(update)) + self.assertEqual(cm.output, ['WARNING:zinolib.controllers.zino1:Unknown update type: "" for id 139110']) + + def test_handle_new_stateless_event_is_very_special(self): + zino1 = self.init_manager() + updates = UpdateHandler(zino1) + update = NotifierResponse(1337, "", "") + result = updates.handle(update) + self.assertEqual(result, None) + + def test_handle_known_type(self): + zino1 = self.init_manager() + zino1.get_events() + old_events = zino1.events.copy() + old_events[raw_event_id].priority = 500 + updates = UpdateHandler(zino1) + update = NotifierResponse(raw_event_id, updates.UpdateType.LOG, "") + ok = updates.handle(update) # will refetch events + self.assertTrue(ok) + self.assertNotEqual(zino1.events[raw_event_id].priority, old_events[raw_event_id].priority) + + def test_handle_unknown_type(self): + zino1 = self.init_manager() + zino1.get_events() + updates = UpdateHandler(zino1) + update = NotifierResponse(raw_event_id, "trout", "") + with self.assertLogs('zinolib.controllers.zino1', level='WARNING'): + ok = updates.handle(update) # will run fallback + self.assertFalse(ok)