From ce0c160e3b643d591e11fe1f8558f586376b1eb2 Mon Sep 17 00:00:00 2001 From: Andy Clyde Date: Mon, 4 Nov 2024 10:50:36 +0000 Subject: [PATCH] Toggled IXP active status based on active membership count --- CHANGELOG.md | 1 + ixp_tracker/importers.py | 29 +++++- tests/fixtures.py | 37 ++++++++ tests/test_members_import.py | 37 +------- tests/test_toggle_ixp_active_status.py | 120 +++++++++++++++++++++++++ 5 files changed, 186 insertions(+), 38 deletions(-) create mode 100644 tests/fixtures.py create mode 100644 tests/test_toggle_ixp_active_status.py diff --git a/CHANGELOG.md b/CHANGELOG.md index e93c415..f50f1d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## 0.6 - extend a membership if created date for imported record is before end date of previous record +- toggle IXP active status based on whether there are active memberships ## 0.5 - fix membership starting after it ended diff --git a/ixp_tracker/importers.py b/ixp_tracker/importers.py index 81add49..63dc7a7 100644 --- a/ixp_tracker/importers.py +++ b/ixp_tracker/importers.py @@ -7,11 +7,11 @@ import requests import dateutil.parser +from django.db.models import Q from django_countries import countries from ixp_tracker.conf import IXP_TRACKER_PEERING_DB_KEY, IXP_TRACKER_PEERING_DB_URL, DATA_ARCHIVE_URL from ixp_tracker import models -from ixp_tracker.models import IXPMembershipRecord logger = logging.getLogger("ixp_tracker") @@ -42,6 +42,8 @@ def import_data( logger.debug("Imported ASNs") import_members(processing_date, geo_lookup) logger.debug("Imported members") + toggle_ixp_active_status(processing_date) + logger.debug("Toggled IXPs active status") else: processing_date = processing_date.replace(day=1) processing_month = processing_date.month @@ -68,6 +70,8 @@ def import_data( process_asn_data(geo_lookup)(asn_data) member_data = backfill_data.get("netixlan", {"data": []}).get("data", []) process_member_data(processing_date, geo_lookup)(member_data) + toggle_ixp_active_status(processing_date) + logger.debug("Toggled IXPs active status") def get_data(endpoint: str, processor: Callable, limit: int = 0, last_updated: datetime = None) -> bool: @@ -194,7 +198,7 @@ def do_process_member_data(all_member_data): created_date = dateutil.parser.isoparse(member_data["created"]).date() membership = models.IXPMembershipRecord.objects.filter(member=member).order_by("-start_date").first() if created or membership is None: - membership = IXPMembershipRecord( + membership = models.IXPMembershipRecord( member=member, start_date=created_date, is_rs_peer=member_data["is_rs_peer"], @@ -216,7 +220,7 @@ def do_process_member_data(all_member_data): membership.end_date = None else: # Most recent membership has ended so create a new membership record - membership = IXPMembershipRecord( + membership = models.IXPMembershipRecord( member=member, start_date=created_date, is_rs_peer=member_data["is_rs_peer"], @@ -265,3 +269,22 @@ def dedupe_member_data(raw_members_data): deduped_data[member_key]["is_rs_peer"] = deduped_data[member_key]["is_rs_peer"] or raw_member["is_rs_peer"] deduped_data[member_key]["speed"] += raw_member["speed"] return list(deduped_data.values()) + + +def toggle_ixp_active_status(processing_date: datetime): + for ixp in models.IXP.objects.all(): + active_members = (models.IXPMembershipRecord.objects + .filter(member__in=ixp.ixpmember_set.all()) + .filter(Q(end_date__isnull=True) | Q(end_date__gte=processing_date)) + ) + if ixp.active_status and len(active_members) == 0: + ixp.active_status = False + ixp.last_active = processing_date + ixp.save() + logger.debug("Marked IXP as inactive", extra={"ixp": ixp.peeringdb_id}) + elif not ixp.active_status and len(active_members) > 0: + ixp.active_status = True + ixp.last_active = processing_date + ixp.save() + logger.debug("Marked IXP as active", extra={"ixp": ixp.peeringdb_id}) + return diff --git a/tests/fixtures.py b/tests/fixtures.py new file mode 100644 index 0000000..9b6463a --- /dev/null +++ b/tests/fixtures.py @@ -0,0 +1,37 @@ +from datetime import datetime + +from ixp_tracker.models import ASN, IXP + + +def create_asn_fixture(as_number: int, country: str = "CH"): + asn = ASN.objects.filter(number=as_number) + if len(asn) > 0: + return asn.first() + asn = ASN( + name="Network Org", + number=as_number, + peeringdb_id=5, + network_type="other", + registration_country_code=country, + created="2019-01-01", + last_updated="2024-05-01" + ) + asn.save() + return asn + + +def create_ixp_fixture(peering_db_id: int, country = "MM"): + ixp = IXP( + name="Old name", + long_name="Network Name", + city="Aberdeen", + website="", + active_status=True, + peeringdb_id=peering_db_id, + country_code=country, + created=datetime(year=2020,month=10,day=1), + last_updated=datetime(year=2023,month=10,day=1), + last_active=datetime(year=2024, month=4, day=1) + ) + ixp.save() + return ixp diff --git a/tests/test_members_import.py b/tests/test_members_import.py index 7e052d6..ce8075b 100644 --- a/tests/test_members_import.py +++ b/tests/test_members_import.py @@ -5,7 +5,8 @@ from ixp_tracker import importers from ixp_tracker.importers import ASNGeoLookup, dedupe_member_data -from ixp_tracker.models import ASN, IXP, IXPMember, IXPMembershipRecord +from ixp_tracker.models import IXPMember, IXPMembershipRecord +from tests.fixtures import create_asn_fixture, create_ixp_fixture pytestmark = pytest.mark.django_db @@ -397,37 +398,3 @@ def test_speed_for_deduped_members_is_sum_of_all_speeds(): deduped_member = deduped_data[0] assert deduped_member["speed"] == 17000 - - -def create_asn_fixture(as_number: int, country: str = "CH"): - asn = ASN.objects.filter(number=as_number) - if len(asn) > 0: - return asn.first() - asn = ASN( - name="Network Org", - number=as_number, - peeringdb_id=5, - network_type="other", - registration_country_code=country, - created="2019-01-01", - last_updated="2024-05-01" - ) - asn.save() - return asn - - -def create_ixp_fixture(peering_db_id: int, country = "MM"): - ixp = IXP( - name="Old name", - long_name="Network Name", - city="Aberdeen", - website="", - active_status=True, - peeringdb_id=peering_db_id, - country_code=country, - created=datetime(year=2020,month=10,day=1), - last_updated=datetime(year=2023,month=10,day=1), - last_active=datetime(year=2024, month=4, day=1) - ) - ixp.save() - return ixp diff --git a/tests/test_toggle_ixp_active_status.py b/tests/test_toggle_ixp_active_status.py new file mode 100644 index 0000000..13f93e6 --- /dev/null +++ b/tests/test_toggle_ixp_active_status.py @@ -0,0 +1,120 @@ +import pytest +from datetime import datetime, timedelta, timezone + +from ixp_tracker.importers import toggle_ixp_active_status +from ixp_tracker.models import IXP, IXPMember, IXPMembershipRecord +from tests.fixtures import create_asn_fixture, create_ixp_fixture + +pytestmark = pytest.mark.django_db +processing_date = datetime.utcnow().replace(tzinfo=timezone.utc) + + +def test_active_ixp_with_members_remains_active(): + ixp = create_ixp_fixture(1) + asn = create_asn_fixture(12345) + member = IXPMember( + ixp=ixp, + asn=asn, + last_updated=ixp.last_updated, + last_active=ixp.last_active + ) + member.save() + membership = IXPMembershipRecord( + member=member, + start_date=ixp.created, + is_rs_peer=True, + speed=1000, + end_date=None + ) + membership.save() + + processing_date = datetime.utcnow().replace(tzinfo=timezone.utc) + toggle_ixp_active_status(processing_date) + + ixp = IXP.objects.get(peeringdb_id=1) + + assert ixp.active_status + + +def test_active_ixp_with_member_marked_ended_is_marked_inactive(): + ixp = create_ixp_fixture(1) + asn = create_asn_fixture(12345) + member = IXPMember( + ixp=ixp, + asn=asn, + last_updated=ixp.last_updated, + last_active=ixp.last_active + ) + member.save() + end_date = processing_date.replace(day=1) - timedelta(days=1) + membership = IXPMembershipRecord( + member=member, + start_date=ixp.created, + is_rs_peer=True, + speed=1000, + end_date=end_date + ) + membership.save() + + toggle_ixp_active_status(processing_date) + + ixp = IXP.objects.get(peeringdb_id=1) + + assert ixp.active_status is False + + +def test_inactive_ixp_with_no_active_members_remains_inactive(): + ixp = create_ixp_fixture(1) + ixp.active_status = False + ixp.save() + asn = create_asn_fixture(12345) + member = IXPMember( + ixp=ixp, + asn=asn, + last_updated=ixp.last_updated, + last_active=ixp.last_active + ) + member.save() + end_date = processing_date.replace(day=1) - timedelta(days=1) + membership = IXPMembershipRecord( + member=member, + start_date=ixp.created, + is_rs_peer=True, + speed=1000, + end_date=end_date + ) + membership.save() + + toggle_ixp_active_status(processing_date) + + ixp = IXP.objects.get(peeringdb_id=1) + + assert ixp.active_status is False + + +def test_inactive_ixp_with_active_member_marked_active(): + ixp = create_ixp_fixture(1) + ixp.active_status = False + ixp.save() + asn = create_asn_fixture(12345) + member = IXPMember( + ixp=ixp, + asn=asn, + last_updated=ixp.last_updated, + last_active=ixp.last_active + ) + member.save() + membership = IXPMembershipRecord( + member=member, + start_date=ixp.created, + is_rs_peer=True, + speed=1000, + end_date=None + ) + membership.save() + + toggle_ixp_active_status(processing_date) + + ixp = IXP.objects.get(peeringdb_id=1) + + assert ixp.active_status