mirror of
https://github.com/peeringdb/peeringdb.git
synced 2024-05-11 05:55:09 +00:00
Dotf fixes 2 (#797)
* [beta] IX-F importer: Adding entry results in 'The server rejected your data' #789 * [beta] IX-F importer: tooltip remains visible after clicking "Auto-resolve", also misspelling #788 * fix protocol-conflict notifications not going out if there are no other conflicts (#771) * fix messy white-space in ix-f notificaiton emails (#790) * add link to python regex documentation (#768) * IX-F import preview when authenticating via basic auth broken #791 Re-add preview tests * [beta] IX-F importer: Lack of "routeserver" status in IX-F JSON should imply "undefined" rather than "false" and result in no action #792 * speed not being present in the ix-f data should be ignored (#792) * [beta] IX-F importer: ignore speed and is_rs_peer differences for now, but retain code #793 * do a simple dedupe of ip addresses before processing the ix-f export remove email debug spam * normalize ip addresses bug fixes for protocol conflict * fix typo and remove debug output * bail if unable to reasonably dedupe duplicate ips in ix-f export data * sanitization fixes * reset ix error notification timer on successful import Co-authored-by: Stefan Pratter <stefan@20c.com> Co-authored-by: Elliot Frank <elliot@20c.com>
This commit is contained in:
@@ -29,7 +29,11 @@ def enable_basic_auth(fn):
|
||||
auth = request.META["HTTP_AUTHORIZATION"].split()
|
||||
if len(auth) == 2:
|
||||
if auth[0].lower() == "basic":
|
||||
username, password = base64.b64decode(auth[1]).split(":", 1)
|
||||
username, password = (
|
||||
base64.b64decode(auth[1].encode("utf-8"))
|
||||
.decode("utf-8")
|
||||
.split(":", 1)
|
||||
)
|
||||
request.user = authenticate(username=username, password=password)
|
||||
if not request.user:
|
||||
return JsonResponse(
|
||||
|
@@ -27,6 +27,7 @@ from peeringdb_server.models import (
|
||||
EnvironmentSetting,
|
||||
debug_mail,
|
||||
IXFImportEmail,
|
||||
ValidationErrorEncoder,
|
||||
)
|
||||
import peeringdb_server.deskpro as deskpro
|
||||
|
||||
@@ -142,6 +143,8 @@ class Importer:
|
||||
self.now = datetime.datetime.now(datetime.timezone.utc)
|
||||
self.invalid_ip_errors = []
|
||||
self.notifications = []
|
||||
self.protocol_conflict = 0
|
||||
self.emails = 0
|
||||
|
||||
def fetch(self, url, timeout=5):
|
||||
"""
|
||||
@@ -223,10 +226,16 @@ class Importer:
|
||||
|
||||
invalid = None
|
||||
vlan_list_found = False
|
||||
ipv4_addresses = {}
|
||||
ipv6_addresses = {}
|
||||
|
||||
# dedupe identical entries in member list
|
||||
member_list = [json.dumps(m) for m in data.get("member_list", [])]
|
||||
member_list = [json.loads(m) for m in set(member_list)]
|
||||
|
||||
# This fixes instances where ixps provide two separate entries for
|
||||
# vlans in vlan_list for ipv4 and ipv6 (AMS-IX for example)
|
||||
for member in data.get("member_list", []):
|
||||
for member in member_list:
|
||||
asn = member.get("asnum")
|
||||
for conn in member.get("connection_list", []):
|
||||
vlans = conn.get("vlan_list", [])
|
||||
@@ -240,6 +249,29 @@ class Importer:
|
||||
vlans[0].update(**vlans[1])
|
||||
conn["vlan_list"] = [vlans[0]]
|
||||
|
||||
# de-dupe reoccurring ipv4 / ipv6 addresses
|
||||
|
||||
ipv4 = vlans[0].get("ipv4", {}).get("address")
|
||||
ipv6 = vlans[0].get("ipv6", {}).get("address")
|
||||
|
||||
ixf_id = (asn, ipv4, ipv6)
|
||||
|
||||
if ipv4 and ipv4 in ipv4_addresses:
|
||||
invalid = _(
|
||||
"Address {} assigned to more than one distinct connection"
|
||||
).format(ipv4)
|
||||
break
|
||||
|
||||
ipv4_addresses[ipv4] = ixf_id
|
||||
|
||||
if ipv6 and ipv6 in ipv6_addresses:
|
||||
invalid = _(
|
||||
"Address {} assigned to more than one distinct connection"
|
||||
).format(ipv6)
|
||||
break
|
||||
|
||||
ipv6_addresses[ipv6] = ixf_id
|
||||
|
||||
if not vlan_list_found:
|
||||
invalid = _("No entries in any of the vlan_list lists, aborting.")
|
||||
|
||||
@@ -282,6 +314,7 @@ class Importer:
|
||||
# null ix-f error note on ixlan if it had error'd before
|
||||
if self.ixlan.ixf_ixp_import_error:
|
||||
self.ixlan.ixf_ixp_import_error = None
|
||||
self.ixlan.ixf_ixp_import_error_notified = None
|
||||
self.ixlan.save()
|
||||
|
||||
# bail if there are no active prefixes on the ixlan
|
||||
@@ -328,6 +361,13 @@ class Importer:
|
||||
# update exchange's ixf fields
|
||||
self.update_ix()
|
||||
|
||||
if (
|
||||
not self.protocol_conflict
|
||||
and self.ixlan.ixf_ixp_import_protocol_conflict
|
||||
):
|
||||
self.ixlan.ixf_ixp_import_protocol_conflict = 0
|
||||
self.ixlan.save()
|
||||
|
||||
self.save_log()
|
||||
|
||||
return True
|
||||
@@ -367,6 +407,30 @@ class Importer:
|
||||
if save_ix:
|
||||
ix.save()
|
||||
|
||||
def fix_consolidated_modify(self, ixf_member_data):
|
||||
"""
|
||||
fix consolidated modify (#770) to retain value
|
||||
for speed and is_rs_peer (#793)
|
||||
"""
|
||||
|
||||
for other in self.pending_save:
|
||||
if other.asn == ixf_member_data.asn:
|
||||
if (
|
||||
other.init_ipaddr4
|
||||
and other.init_ipaddr4 == ixf_member_data.init_ipaddr4
|
||||
) or (
|
||||
other.init_ipaddr6
|
||||
and other.init_ipaddr6 == ixf_member_data.init_ipaddr6
|
||||
):
|
||||
|
||||
if not other.modify_speed:
|
||||
other.speed = ixf_member_data.speed
|
||||
|
||||
if not other.modify_is_rs_peer:
|
||||
other.is_rs_peer = ixf_member_data.is_rs_peer
|
||||
|
||||
break
|
||||
|
||||
@reversion.create_revision()
|
||||
def process_saves(self):
|
||||
for ixf_member in self.pending_save:
|
||||
@@ -406,6 +470,10 @@ class Importer:
|
||||
data={},
|
||||
)
|
||||
|
||||
# fix consolidated modify (#770) to retain values
|
||||
# for speed and is_rs_peer (#793)
|
||||
self.fix_consolidated_modify(ixf_member_data)
|
||||
|
||||
self.deletions[ixf_member_data.ixf_id] = ixf_member_data
|
||||
if netixlan.network.allow_ixp_update:
|
||||
self.log_apply(
|
||||
@@ -572,6 +640,7 @@ class Importer:
|
||||
|
||||
asn = member["asnum"]
|
||||
for connection in connection_list:
|
||||
|
||||
self.connection_errors = {}
|
||||
state = connection.get("state", "active").lower()
|
||||
if state in self.allowed_states:
|
||||
@@ -633,12 +702,14 @@ class Importer:
|
||||
ixf_id = [asn]
|
||||
|
||||
if ipv4_addr:
|
||||
ixf_id.append(ipaddress.ip_address(f"{ipv4_addr}"))
|
||||
ipv4_addr = ipaddress.ip_address(f"{ipv4_addr}")
|
||||
ixf_id.append(ipv4_addr)
|
||||
else:
|
||||
ixf_id.append(None)
|
||||
|
||||
if ipv6_addr:
|
||||
ixf_id.append(ipaddress.ip_address(f"{ipv6_addr}"))
|
||||
ipv6_addr = ipaddress.ip_address(f"{ipv6_addr}")
|
||||
ixf_id.append(ipv6_addr)
|
||||
else:
|
||||
ixf_id.append(None)
|
||||
|
||||
@@ -681,18 +752,25 @@ class Importer:
|
||||
|
||||
continue
|
||||
|
||||
protocol_conflicts = []
|
||||
protocol_conflict = 0
|
||||
|
||||
# keep track of conflicts between ix/net in terms of ip
|
||||
# protocols supported.
|
||||
|
||||
if ipv4_addr and not ipv4_support:
|
||||
protocol_conflicts.append(4)
|
||||
protocol_conflict = 4
|
||||
elif ipv6_addr and not ipv6_support:
|
||||
protocol_conflict = 6
|
||||
|
||||
if ipv6_addr and not ipv6_support:
|
||||
protocol_conflicts.append(6)
|
||||
if protocol_conflict and not self.protocol_conflict:
|
||||
self.protocol_conflict = protocol_conflict
|
||||
|
||||
if protocol_conflict and not self.ixlan.ixf_ixp_import_protocol_conflict:
|
||||
self.ixlan.ixf_ixp_import_protocol_conflict = protocol_conflict
|
||||
|
||||
if self.save:
|
||||
self.ixlan.save()
|
||||
|
||||
if protocol_conflicts:
|
||||
self.queue_notification(
|
||||
IXFMemberData.instantiate(
|
||||
asn,
|
||||
@@ -733,9 +811,10 @@ class Importer:
|
||||
else:
|
||||
operational = True
|
||||
|
||||
is_rs_peer = ipv4.get("routeserver", False) or ipv6.get(
|
||||
"routeserver", False
|
||||
)
|
||||
if "routeserver" not in ipv4 and "routeserver" not in ipv6:
|
||||
is_rs_peer = None
|
||||
else:
|
||||
is_rs_peer = ipv4.get("routeserver", ipv6.get("routeserver"))
|
||||
|
||||
try:
|
||||
ixf_member_data = IXFMemberData.instantiate(
|
||||
@@ -749,12 +828,18 @@ class Importer:
|
||||
ixlan=self.ixlan,
|
||||
save=self.save,
|
||||
)
|
||||
|
||||
if not ixf_member_data.ipaddr4 and not ixf_member_data.ipaddr6:
|
||||
continue
|
||||
|
||||
except NetworkProtocolsDisabled as exc:
|
||||
self.log_error(f"{exc}")
|
||||
continue
|
||||
|
||||
if self.connection_errors:
|
||||
ixf_member_data.error = json.dumps(self.connection_errors)
|
||||
ixf_member_data.error = json.dumps(
|
||||
self.connection_errors, cls=ValidationErrorEncoder
|
||||
)
|
||||
else:
|
||||
ixf_member_data.error = ixf_member_data.previous_error
|
||||
|
||||
@@ -774,8 +859,11 @@ class Importer:
|
||||
for iface in if_list:
|
||||
try:
|
||||
speed += int(iface.get("if_speed", 0))
|
||||
except ValueError:
|
||||
log_msg = _("Invalid speed value: {}").format(iface.get("if_speed"))
|
||||
except (ValueError, AttributeError):
|
||||
try:
|
||||
log_msg = _("Invalid speed value: {}").format(iface.get("if_speed"))
|
||||
except AttributeError:
|
||||
log_msg = _("Invalid speed value: could not be parsed")
|
||||
self.log_error(log_msg)
|
||||
if "speed" not in self.connection_errors:
|
||||
self.connection_errors["speed"] = []
|
||||
@@ -1069,7 +1157,7 @@ class Importer:
|
||||
)
|
||||
mail.send(fail_silently=False)
|
||||
else:
|
||||
print("EMAIL", subject, recipients)
|
||||
self.emails += 1
|
||||
# debug_mail(
|
||||
# subject, message, settings.DEFAULT_FROM_EMAIL, recipients,
|
||||
# )
|
||||
@@ -1214,7 +1302,7 @@ class Importer:
|
||||
|
||||
# prepare consolidation rocketship
|
||||
|
||||
if asn not in net_notifications:
|
||||
if notify_net and asn not in net_notifications:
|
||||
net_notifications[asn] = {
|
||||
"proposals": {},
|
||||
"count": 0,
|
||||
@@ -1222,7 +1310,7 @@ class Importer:
|
||||
"contacts": ixf_member_data.net_contacts,
|
||||
}
|
||||
|
||||
if ix not in net_notifications[asn]["proposals"]:
|
||||
if notify_net and ix not in net_notifications[asn]["proposals"]:
|
||||
net_notifications[asn]["proposals"][ix] = {
|
||||
"add": [],
|
||||
"modify": [],
|
||||
@@ -1230,7 +1318,7 @@ class Importer:
|
||||
"protocol_conflict": None,
|
||||
}
|
||||
|
||||
if ix not in ix_notifications:
|
||||
if notify_ix and ix not in ix_notifications:
|
||||
ix_notifications[ix] = {
|
||||
"proposals": {},
|
||||
"count": 0,
|
||||
@@ -1238,7 +1326,7 @@ class Importer:
|
||||
"contacts": ixf_member_data.ix_contacts,
|
||||
}
|
||||
|
||||
if asn not in ix_notifications[ix]["proposals"]:
|
||||
if notify_ix and asn not in ix_notifications[ix]["proposals"]:
|
||||
ix_notifications[ix]["proposals"][asn] = {
|
||||
"add": [],
|
||||
"modify": [],
|
||||
@@ -1248,14 +1336,17 @@ class Importer:
|
||||
|
||||
# render and push proposal text for network
|
||||
|
||||
if notify_net and ixf_member_data.actionable_for_network:
|
||||
if notify_net and (
|
||||
ixf_member_data.actionable_for_network or action == "protocol_conflict"
|
||||
):
|
||||
proposals = net_notifications[asn]["proposals"][ix]
|
||||
message = ixf_member_data.render_notification(
|
||||
template_file, recipient="net", context=context,
|
||||
)
|
||||
|
||||
if action == "protocol_conflict":
|
||||
if action == "protocol_conflict" and not proposals[action]:
|
||||
proposals[action] = message
|
||||
net_notifications[asn]["count"] += 1
|
||||
else:
|
||||
proposals[action].append(message)
|
||||
net_notifications[asn]["count"] += 1
|
||||
@@ -1268,8 +1359,9 @@ class Importer:
|
||||
template_file, recipient="ix", context=context,
|
||||
)
|
||||
|
||||
if action == "protocol_conflict":
|
||||
if action == "protocol_conflict" and not proposals[action]:
|
||||
proposals[action] = message
|
||||
ix_notifications[ix]["count"] += 1
|
||||
else:
|
||||
proposals[action].append(message)
|
||||
ix_notifications[ix]["count"] += 1
|
||||
@@ -1481,7 +1573,9 @@ class Importer:
|
||||
message = template.render(
|
||||
{"error": error, "dt": now, "instance": ixf_member_data}
|
||||
)
|
||||
self._ticket(ixf_member_data, subject, message)
|
||||
|
||||
# AC does not want ticket here as per #794
|
||||
# self._ticket(ixf_member_data, subject, message)
|
||||
|
||||
if ixf_member_data.ix_contacts:
|
||||
self._email(
|
||||
|
@@ -108,6 +108,9 @@ class Command(BaseCommand):
|
||||
if self.commit:
|
||||
IXFMemberData.objects.all().delete()
|
||||
|
||||
# also reset and protocol conflict hints (#771)
|
||||
IXLan.objects.filter(ixf_ixp_import_protocol_conflict__gt=0).update(ixf_ixp_import_protocol_conflict=0)
|
||||
|
||||
def reset_all_dismisses(self):
|
||||
self.log("Resetting dismisses: setting IXFMemberData.dismissed=False on all IXFMemberData instances")
|
||||
if self.commit:
|
||||
@@ -119,6 +122,7 @@ class Command(BaseCommand):
|
||||
self.log("Resetting email: emptying the IXFImportEmail table")
|
||||
if self.commit:
|
||||
IXFImportEmail.objects.all().delete()
|
||||
IXLan.objects.filter(ixf_ixp_import_error_notified__isnull=False).update(ixf_ixp_import_error_notified=None)
|
||||
|
||||
def reset_all_tickets(self):
|
||||
self.log("Resetting tickets: removing DeskProTicket objects where subject contains '[IX-F]'")
|
||||
@@ -226,3 +230,5 @@ class Command(BaseCommand):
|
||||
importer.reset(save=self.commit)
|
||||
importer.notifications = total_notifications
|
||||
importer.notify_proposals()
|
||||
|
||||
self.stdout.write(f"Emails: {importer.emails}")
|
||||
|
23
peeringdb_server/migrations/0050_auto_20200730_0700.py
Normal file
23
peeringdb_server/migrations/0050_auto_20200730_0700.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 2.2.14 on 2020-07-30 07:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('peeringdb_server', '0049_deskrpo_ticket_additions'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ixlan',
|
||||
name='ixf_ixp_import_protocol_conflict',
|
||||
field=models.IntegerField(blank=True, default=0, help_text='IX has been sending IP addresses for protocol not supported by network', null=True, verbose_name='IX-F sent IPs for unsupported protocol'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='network',
|
||||
name='allow_ixp_update',
|
||||
field=models.BooleanField(default=False, help_text='Sepcifies whether an IXP is allowed to add a netixlan entry for this network via their ixp_member data'),
|
||||
),
|
||||
]
|
18
peeringdb_server/migrations/0051_auto_20200730_1622.py
Normal file
18
peeringdb_server/migrations/0051_auto_20200730_1622.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 2.2.14 on 2020-07-30 16:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('peeringdb_server', '0050_auto_20200730_0700'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ixfmemberdata',
|
||||
name='is_rs_peer',
|
||||
field=models.BooleanField(blank=True, default=None, help_text='RS Peer', null=True),
|
||||
),
|
||||
]
|
@@ -1773,6 +1773,15 @@ class IXLan(pdb_models.IXLanBase):
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
ixf_ixp_import_protocol_conflict = models.IntegerField(
|
||||
_("IX-F sent IPs for unsupported protocol"),
|
||||
help_text=_(
|
||||
"IX has been sending IP addresses for protocol not supported by network"
|
||||
),
|
||||
null=True,
|
||||
blank=True,
|
||||
default=0,
|
||||
)
|
||||
|
||||
# FIXME: delete cascade needs to be fixed in django-peeringdb, can remove
|
||||
# this afterwards
|
||||
@@ -2296,6 +2305,10 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
),
|
||||
)
|
||||
|
||||
is_rs_peer = models.BooleanField(
|
||||
default=None, null=True, blank=True, help_text=_("RS Peer")
|
||||
)
|
||||
|
||||
error = models.TextField(
|
||||
null=True,
|
||||
blank=True,
|
||||
@@ -2401,7 +2414,6 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
|
||||
try:
|
||||
id_filters = cls.id_filters(asn, ipaddr4, ipaddr6)
|
||||
|
||||
instances = cls.objects.filter(**id_filters)
|
||||
|
||||
if not instances.exists():
|
||||
@@ -2452,7 +2464,7 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
|
||||
instance.speed = kwargs.get("speed", 0)
|
||||
instance.operational = kwargs.get("operational", True)
|
||||
instance.is_rs_peer = kwargs.get("is_rs_peer", False)
|
||||
instance.is_rs_peer = kwargs.get("is_rs_peer")
|
||||
instance.ixlan = ixlan
|
||||
instance.fetched = fetched
|
||||
instance.for_deletion = for_deletion
|
||||
@@ -2764,12 +2776,16 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
if self.marked_for_removal or not netixlan:
|
||||
return changes
|
||||
|
||||
if netixlan.is_rs_peer != self.is_rs_peer:
|
||||
if (
|
||||
self.modify_is_rs_peer
|
||||
and self.is_rs_peer is not None
|
||||
and netixlan.is_rs_peer != self.is_rs_peer
|
||||
):
|
||||
changes.update(
|
||||
is_rs_peer={"from": netixlan.is_rs_peer, "to": self.is_rs_peer}
|
||||
)
|
||||
|
||||
if netixlan.speed != self.speed:
|
||||
if self.modify_speed and self.speed > 0 and netixlan.speed != self.speed:
|
||||
changes.update(speed={"from": netixlan.speed, "to": self.speed})
|
||||
|
||||
if netixlan.operational != self.operational:
|
||||
@@ -2782,6 +2798,22 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
|
||||
return changes
|
||||
|
||||
@property
|
||||
def modify_speed(self):
|
||||
"""
|
||||
Returns whether or not the `speed` property
|
||||
is enabled to receive modify updates or not (#793)
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def modify_is_rs_peer(self):
|
||||
"""
|
||||
Returns whether or not the `is_rs_peer` property
|
||||
is enabled to receive modify updates or not (#793)
|
||||
"""
|
||||
return False
|
||||
|
||||
@property
|
||||
def changed_fields(self):
|
||||
"""
|
||||
@@ -3012,13 +3044,17 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
|
||||
self._netixlan = NetworkIXLan.objects.get(**filters)
|
||||
except NetworkIXLan.DoesNotExist:
|
||||
is_rs_peer = self.is_rs_peer
|
||||
if is_rs_peer is None:
|
||||
is_rs_peer = False
|
||||
|
||||
self._netixlan = NetworkIXLan(
|
||||
ipaddr4=self.ipaddr4,
|
||||
ipaddr6=self.ipaddr6,
|
||||
speed=self.speed,
|
||||
asn=self.asn,
|
||||
operational=self.operational,
|
||||
is_rs_peer=self.is_rs_peer,
|
||||
is_rs_peer=is_rs_peer,
|
||||
ixlan=self.ixlan,
|
||||
network=self.net,
|
||||
status="ok",
|
||||
@@ -3136,8 +3172,11 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
|
||||
self.validate_speed()
|
||||
|
||||
netixlan.speed = self.speed
|
||||
netixlan.is_rs_peer = self.is_rs_peer
|
||||
if self.modify_speed and self.speed:
|
||||
netixlan.speed = self.speed
|
||||
if self.modify_is_rs_peer and self.is_rs_peer is not None:
|
||||
netixlan.is_rs_peer = self.is_rs_peer
|
||||
|
||||
netixlan.operational = self.operational
|
||||
if save:
|
||||
netixlan.full_clean()
|
||||
@@ -3267,6 +3306,7 @@ class IXFMemberData(pdb_models.NetworkIXLanBase):
|
||||
|
||||
if not self.id and save:
|
||||
self.grab_validation_errors()
|
||||
|
||||
self.save()
|
||||
return True
|
||||
|
||||
|
@@ -689,6 +689,12 @@ PeeringDB.IXFProposals = twentyc.cls.define(
|
||||
button_resolve_all.prop('disabled',true);
|
||||
}
|
||||
|
||||
// make sure all tooltips are closed
|
||||
|
||||
proposals.find('[data-toggle="tooltip"]').each(function() {
|
||||
$(this).tooltip("hide");
|
||||
});
|
||||
|
||||
if(button_resolve_all.prop('disabled') && button_add_all.prop('disabled'))
|
||||
proposals.detach()
|
||||
},
|
||||
|
@@ -2,5 +2,8 @@
|
||||
{% load i18n %}
|
||||
{% block search %}
|
||||
{{block.super}}
|
||||
<p>{% trans "To use regex search, begin search term with ^ and end with $."%}</p>
|
||||
{% blocktrans with doc_url="https://docs.python.org/3/library/re.html" %}
|
||||
To use <a href="{{ doc_url }}" target="_blank">regex search</a>, begin search term with ^ and end with $.
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
@@ -5,22 +5,17 @@ This email details {{ count }} new data mismatch[es] discovered by the PeeringDB
|
||||
This email details {{ count }} new data mismatch[es] discovered by the PeeringDB IX-F Importer. Please review and correct these mismatches in your IX-F JSON export ({{ entity.ixlan.ixf_ixp_member_list_url }}) or work with the indicated networks to correct the data.
|
||||
{% endif %}
|
||||
-----------------------------
|
||||
{% for other, messages in proposals.items %}
|
||||
{% for message in messages.add %}
|
||||
{% for other, messages in proposals.items %}{% for message in messages.add %}
|
||||
{% include "email/notify-ixf-conflict-insert.txt" with entity=entity other=other ticket_days=ticket_days %}
|
||||
{{ message }}
|
||||
-----------------------------{% endfor %}
|
||||
{% for message in messages.modify %}
|
||||
-----------------------------{% endfor %}{% for message in messages.modify %}
|
||||
{% include "email/notify-ixf-conflict-insert.txt" with entity=entity other=other ticket_days=ticket_days %}
|
||||
{{ message }}
|
||||
-----------------------------{% endfor %}
|
||||
{% for message in messages.delete %}
|
||||
-----------------------------{% endfor %}{% for message in messages.delete %}
|
||||
{% include "email/notify-ixf-conflict-insert.txt" with entity=entity other=other ticket_days=ticket_days %}
|
||||
{{ message }}
|
||||
-----------------------------{% endfor %}
|
||||
{% if messages.protocol_conflict %}
|
||||
-----------------------------{% endfor %}{% if messages.protocol_conflict %}
|
||||
{% include "email/notify-ixf-conflict-insert.txt" with entity=entity other=other ticket_days=ticket_days %}
|
||||
{{ messages.protocol_conflict }}
|
||||
-----------------------------{% endif %}
|
||||
{% endfor %}
|
||||
-----------------------------{% endif %}{% endfor %}
|
||||
{% endspaceless %}
|
||||
|
@@ -73,7 +73,7 @@
|
||||
<a href="{% url "ix-view" id=ixf_proposals.ix.id %}">{% trans "Their PeeringDB entry" %}</a>
|
||||
</div>
|
||||
<div class="col-xs-6 right">
|
||||
<button data-toggle="tooltip" title="{% trans "Autoamtically update and remove all suggested entries" %}" class="btn btn-default btn-sm resolve-all" type="button">{% trans "Auto-resolve" %}</button>
|
||||
<button data-toggle="tooltip" title="{% trans "Automatically update and remove all suggested entries" %}" class="btn btn-default btn-sm resolve-all" type="button">{% trans "Auto-resolve" %}</button>
|
||||
|
||||
<button data-toggle="tooltip" title="{% trans "Automatically add all suggested entries" %}" class="btn btn-default btn-sm add-all" type="button">{% trans "Auto-add" %}</button>
|
||||
</div>
|
||||
@@ -115,7 +115,7 @@
|
||||
{% if x.net.info_ipv6 %}
|
||||
<input type="text" placeholder="{% trans "IPv6" %}" data-field="ipaddr6" value="{{ x.ipaddr6|none_blank }}">
|
||||
{% else %}
|
||||
<input type="text" placeholder="{% trans "IPv6" %}" data-field="ipaddr4" value="">
|
||||
<input type="text" placeholder="{% trans "IPv6" %}" data-field="ipaddr6" value="">
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -298,8 +298,22 @@
|
||||
<input type="hidden" data-field="ixlan_id" value="{{ x.ixlan_id }}">
|
||||
<input type="hidden" data-field="net_id" value="{{ x.net.id }}">
|
||||
<input type="hidden" data-field="asn" value="{{ x.asn }}">
|
||||
|
||||
{% if x.modify_speed %}
|
||||
<input type="hidden" data-field="speed" value="{{ x.speed }}">
|
||||
{% else %}
|
||||
<input type="hidden" data-field="speed" value="{% if x.primary_requirement %}{{ x.primary_requirement.speed }}{% else %}{{ x.netixlan.speed }}{% endif %}">
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% if x.modify_is_rs_peer %}
|
||||
<input type="checkbox" style="display:none" data-field="is_rs_peer" {% if x.is_rs_peer %}checked{% endif %}>
|
||||
{% elif x.primary_requirement %}
|
||||
<input type="checkbox" style="display:none" data-field="is_rs_peer" {% if x.primary_requirement.is_rs_peer %}checked{% endif %}>
|
||||
{% else %}
|
||||
<input type="checkbox" style="display:none" data-field="is_rs_peer" {% if x.netixlan.is_rs_peer %}checked{% endif %}>
|
||||
{% endif %}
|
||||
|
||||
<input type="checkbox" style="display:none" data-field="operational" {% if x.operational %}checked{% endif %}>
|
||||
|
||||
{% if x.ipaddr4_on_requirement %}
|
||||
|
65
tests/data/json_members_list/ixf.member.4.json
Normal file
65
tests/data/json_members_list/ixf.member.4.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"timestamp": "2020-07-13T09:23:47Z",
|
||||
"version": "1.0",
|
||||
"ixp_list": [
|
||||
{
|
||||
"shortname": "Test Exchange",
|
||||
"ixp_id": 1,
|
||||
"ixf_id": 1
|
||||
}
|
||||
],
|
||||
"member_list": [
|
||||
{
|
||||
"asnum": 2906,
|
||||
"member_type": "peering",
|
||||
"name": "Netflix",
|
||||
"url": "http://netflix.com/",
|
||||
"contact_email": [
|
||||
"peering@netflix.com",
|
||||
"mrpeering@netflix.com"
|
||||
],
|
||||
"contact_phone": [
|
||||
"+1 1234 5678"
|
||||
],
|
||||
"contact_hours": "8/5",
|
||||
"peering_policy": "open",
|
||||
"peering_policy_url": "https://www.netflix.com/openconnect/",
|
||||
"member_since": "2009-02-04T00:00:00Z",
|
||||
"connection_list": [
|
||||
{
|
||||
"ixp_id": 42,
|
||||
"connected_since": "2009-02-04T00:00:00Z",
|
||||
"state": "connected",
|
||||
"if_list": [
|
||||
{
|
||||
"switch_id": 1,
|
||||
"if_speed": 10000,
|
||||
"if_type": "LR4"
|
||||
}
|
||||
],
|
||||
"vlan_list": [
|
||||
{
|
||||
"vlan_id": 0,
|
||||
"ipv4": {
|
||||
"address": "195.69.147.250",
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V4",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
},
|
||||
"ipv6": {
|
||||
"address": "2001:7f8:1::a500:2906:1",
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V6",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
65
tests/data/json_members_list/ixf.member.5.json
Normal file
65
tests/data/json_members_list/ixf.member.5.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"timestamp": "2020-07-13T09:23:47Z",
|
||||
"version": "1.0",
|
||||
"ixp_list": [
|
||||
{
|
||||
"shortname": "Test Exchange",
|
||||
"ixp_id": 1,
|
||||
"ixf_id": 1
|
||||
}
|
||||
],
|
||||
"member_list": [
|
||||
{
|
||||
"asnum": 1001,
|
||||
"member_type": "peering",
|
||||
"name": "Netflix",
|
||||
"url": "http://netflix.com/",
|
||||
"contact_email": [
|
||||
"peering@netflix.com",
|
||||
"mrpeering@netflix.com"
|
||||
],
|
||||
"contact_phone": [
|
||||
"+1 1234 5678"
|
||||
],
|
||||
"contact_hours": "8/5",
|
||||
"peering_policy": "open",
|
||||
"peering_policy_url": "https://www.netflix.com/openconnect/",
|
||||
"member_since": "2009-02-04T00:00:00Z",
|
||||
"connection_list": [
|
||||
{
|
||||
"ixp_id": 42,
|
||||
"connected_since": "2009-02-04T00:00:00Z",
|
||||
"state": "connected",
|
||||
"if_list": [
|
||||
{
|
||||
"switch_id": 1,
|
||||
"if_speed": 10000,
|
||||
"if_type": "LR4"
|
||||
}
|
||||
],
|
||||
"vlan_list": [
|
||||
{
|
||||
"vlan_id": 0,
|
||||
"ipv4": {
|
||||
"address": "195.69.147.250",
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V4",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
},
|
||||
"ipv6": {
|
||||
"address": "2001:7f8:1::a500:2906:1",
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V6",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
88
tests/data/json_members_list/ixf.member.invalid.2.json
Normal file
88
tests/data/json_members_list/ixf.member.invalid.2.json
Normal file
@@ -0,0 +1,88 @@
|
||||
{
|
||||
"timestamp": "2020-07-13T09:23:47Z",
|
||||
"version": "1.0",
|
||||
"ixp_list": [
|
||||
{
|
||||
"shortname": "Test Exchange",
|
||||
"ixp_id": 1,
|
||||
"ixf_id": 1
|
||||
}
|
||||
],
|
||||
"member_list": [
|
||||
{
|
||||
"asnum": 1001,
|
||||
"member_type": "peering",
|
||||
"name": "Netflix",
|
||||
"url": "http://netflix.com/",
|
||||
"contact_email": [
|
||||
"peering@netflix.com",
|
||||
"mrpeering@netflix.com"
|
||||
],
|
||||
"contact_phone": [
|
||||
"+1 1234 5678"
|
||||
],
|
||||
"contact_hours": "8/5",
|
||||
"peering_policy": "open",
|
||||
"peering_policy_url": "https://www.netflix.com/openconnect/",
|
||||
"member_since": "2009-02-04T00:00:00Z",
|
||||
"connection_list": [
|
||||
{
|
||||
"ixp_id": 42,
|
||||
"connected_since": "2009-02-04T00:00:00Z",
|
||||
"state": "connected",
|
||||
"if_list": [
|
||||
{
|
||||
"switch_id": 1,
|
||||
"if_speed": "This is invalid",
|
||||
"if_type": "LR4"
|
||||
}
|
||||
],
|
||||
"vlan_list": [
|
||||
{
|
||||
"vlan_id": 0,
|
||||
"ipv4": {
|
||||
"address": "195.69.147.100",
|
||||
"routeserver": false,
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V4",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
},
|
||||
"ipv6": {
|
||||
"address": "2001:7f8:1::a500:2906:4",
|
||||
"routeserver": false,
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V6",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"vlan_id": 1,
|
||||
"ipv4": {
|
||||
"address": "195.69.147.200",
|
||||
"routeserver": false,
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V4",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
},
|
||||
"ipv6": {
|
||||
"address": "2001:7f8:1::a500:2906:2",
|
||||
"routeserver": false,
|
||||
"max_prefix": 42,
|
||||
"as_macro": "AS-NFLX-V6",
|
||||
"mac_address" : [
|
||||
"00:0a:95:9d:68:16"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
158
tests/test_ixf_import_tools.py
Normal file
158
tests/test_ixf_import_tools.py
Normal file
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
import json
|
||||
import reversion
|
||||
import requests
|
||||
import jsonschema
|
||||
import time
|
||||
import io
|
||||
import base64
|
||||
|
||||
from django.db import transaction
|
||||
from django.core.cache import cache
|
||||
from django.test import TestCase, Client, RequestFactory
|
||||
from django.core.management import call_command
|
||||
|
||||
from peeringdb_server.models import (
|
||||
Organization,
|
||||
Network,
|
||||
NetworkIXLan,
|
||||
IXLan,
|
||||
IXLanPrefix,
|
||||
InternetExchange,
|
||||
IXLanIXFMemberImportAttempt,
|
||||
IXLanIXFMemberImportLog,
|
||||
IXLanIXFMemberImportLogEntry,
|
||||
User,
|
||||
)
|
||||
|
||||
from peeringdb_server.import_views import (
|
||||
view_import_ixlan_ixf_preview,
|
||||
view_import_net_ixf_preview,
|
||||
view_import_net_ixf_postmortem,
|
||||
)
|
||||
|
||||
from .util import ClientCase
|
||||
|
||||
|
||||
class TestImportPreview(ClientCase):
|
||||
|
||||
"""
|
||||
Test the ixf import preview
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
super(TestImportPreview, cls).setUpTestData()
|
||||
cls.org = Organization.objects.create(name="Test Org", status="ok")
|
||||
cls.ix = InternetExchange.objects.create(
|
||||
name="Test IX", status="ok", org=cls.org
|
||||
)
|
||||
|
||||
cls.ixlan = cls.ix.ixlan
|
||||
|
||||
IXLanPrefix.objects.create(
|
||||
ixlan=cls.ixlan, status="ok", prefix="195.69.144.0/22", protocol="IPv4"
|
||||
)
|
||||
IXLanPrefix.objects.create(
|
||||
ixlan=cls.ixlan, status="ok", prefix="2001:7f8:1::/64", protocol="IPv6"
|
||||
)
|
||||
|
||||
cls.net = Network.objects.create(
|
||||
org=cls.org, status="ok", asn=1000, name="net01"
|
||||
)
|
||||
cls.net_2 = Network.objects.create(
|
||||
org=cls.org, status="ok", asn=1001, name="net02"
|
||||
)
|
||||
|
||||
cls.admin_user = User.objects.create_user("admin", "admin@localhost", "admin")
|
||||
|
||||
cls.org.admin_usergroup.user_set.add(cls.admin_user)
|
||||
|
||||
def test_import_preview(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/ixlan/{}/ixf/preview/".format(self.ixlan.id)
|
||||
)
|
||||
request.user = self.admin_user
|
||||
|
||||
response = view_import_ixlan_ixf_preview(request, self.ixlan.id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content)["errors"] == [
|
||||
"IXF import url not specified"
|
||||
]
|
||||
|
||||
def test_import_preview_basic_auth(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/ixlan/{}/ixf/preview/".format(self.ixlan.id)
|
||||
)
|
||||
auth = base64.b64encode("admin:admin".encode("utf-8")).decode("utf-8")
|
||||
request.META["HTTP_AUTHORIZATION"] = f"Basic {auth}"
|
||||
|
||||
response = view_import_ixlan_ixf_preview(request, self.ixlan.id)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert json.loads(response.content)["errors"] == [
|
||||
"IXF import url not specified"
|
||||
]
|
||||
|
||||
def test_import_preview_fail_ratelimit(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/ixlan/{}/ixf/preview/".format(self.ixlan.id)
|
||||
)
|
||||
request.user = self.admin_user
|
||||
|
||||
response = view_import_ixlan_ixf_preview(request, self.ixlan.id)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = view_import_ixlan_ixf_preview(request, self.ixlan.id)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_import_preview_fail_permission(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/ixlan/{}/ixf/preview/".format(self.ixlan.id)
|
||||
)
|
||||
request.user = self.guest_user
|
||||
|
||||
response = view_import_ixlan_ixf_preview(request, self.ixlan.id)
|
||||
assert response.status_code == 403
|
||||
|
||||
def test_import_net_preview(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/net/{}/ixf/preview/".format(self.net.id)
|
||||
)
|
||||
request.user = self.admin_user
|
||||
|
||||
response = view_import_net_ixf_preview(request, self.net.id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_import_net_preview_basic_auth(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/net/{}/ixf/preview/".format(self.net.id)
|
||||
)
|
||||
auth = base64.b64encode("admin:admin".encode("utf-8")).decode("utf-8")
|
||||
request.META["HTTP_AUTHORIZATION"] = f"Basic {auth}"
|
||||
response = view_import_net_ixf_preview(request, self.net.id)
|
||||
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_import_net_preview_fail_ratelimit(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/net/{}/ixf/preview/".format(self.net.id)
|
||||
)
|
||||
request.user = self.admin_user
|
||||
|
||||
response = view_import_net_ixf_preview(request, self.net.id)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = view_import_net_ixf_preview(request, self.net.id)
|
||||
assert response.status_code == 400
|
||||
|
||||
def test_import_net_preview_fail_permission(self):
|
||||
request = RequestFactory().get(
|
||||
"/import/net/{}/ixf/preview/".format(self.net.id)
|
||||
)
|
||||
request.user = self.guest_user
|
||||
|
||||
response = view_import_net_ixf_preview(request, self.net.id)
|
||||
assert response.status_code == 403
|
@@ -75,7 +75,7 @@ def test_resolve_local_ixf(entities, use_ip, save):
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
|
||||
# We do not email upon resolve
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -138,20 +138,26 @@ def test_update_data_attributes(entities, use_ip, save):
|
||||
log = importer.log["data"][0]
|
||||
|
||||
assert log["action"] == "modify"
|
||||
assert "is_rs_peer" in log["reason"]
|
||||
assert "operational" in log["reason"]
|
||||
assert "speed" in log["reason"]
|
||||
|
||||
# #793 we are currently ignoring is_rs_peer
|
||||
# and speed for modifies
|
||||
assert "is_rs_peer" not in log["reason"]
|
||||
assert "speed" not in log["reason"]
|
||||
|
||||
netixlan = NetworkIXLan.objects.filter(status="ok").first()
|
||||
assert netixlan.operational == True
|
||||
assert netixlan.is_rs_peer == True
|
||||
assert netixlan.speed == 10000
|
||||
|
||||
# #793 we are currently ignoring is_rs_peer
|
||||
# and speed for modifies
|
||||
assert netixlan.is_rs_peer == False
|
||||
assert netixlan.speed == 20000
|
||||
|
||||
# Assert idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
|
||||
# Assert no emails
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# test rollback
|
||||
import_log = IXLanIXFMemberImportLog.objects.first()
|
||||
@@ -164,6 +170,48 @@ def test_update_data_attributes(entities, use_ip, save):
|
||||
assert netixlan.ipaddr6 == use_ip(6, "2001:7f8:1::a500:2906:1")
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_data_attributes_no_routeserver(entities, save):
|
||||
"""
|
||||
The NetIXLan differs from the remote data, but allow_ixp_update is enabled
|
||||
so we update automatically.
|
||||
|
||||
routeserver attribute is missing from remote, we ignore it
|
||||
"""
|
||||
data = setup_test_data("ixf.member.4")
|
||||
network = entities["net"]["UPDATE_ENABLED"]
|
||||
ixlan = entities["ixlan"][0]
|
||||
|
||||
with reversion.create_revision():
|
||||
entities["netixlan"].append(
|
||||
NetworkIXLan.objects.create(
|
||||
network=network,
|
||||
ixlan=ixlan,
|
||||
asn=network.asn,
|
||||
speed=20000,
|
||||
ipaddr4="195.69.147.250",
|
||||
ipaddr6="2001:7f8:1::a500:2906:1",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
operational=False,
|
||||
)
|
||||
)
|
||||
|
||||
importer = ixf.Importer()
|
||||
|
||||
if not save:
|
||||
return assert_idempotent(importer, ixlan, data, save=False)
|
||||
|
||||
importer.update(ixlan, data=data)
|
||||
importer.notify_proposals()
|
||||
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
|
||||
netixlan = entities["netixlan"][-1]
|
||||
netixlan.refresh_from_db()
|
||||
assert netixlan.is_rs_peer == True
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_suggest_modify_local_ixf(entities, use_ip, save):
|
||||
"""
|
||||
@@ -284,7 +332,7 @@ def test_suggest_modify_local_ixf(entities, use_ip, save):
|
||||
|
||||
else:
|
||||
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
assert preexisting_ixfmember_data == IXFMemberData.objects.first()
|
||||
|
||||
@@ -373,6 +421,48 @@ def test_suggest_modify(entities, use_ip, save):
|
||||
assert_idempotent(importer, ixlan, data, save=save)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_suggest_modify_no_routeserver(entities, save):
|
||||
"""
|
||||
Netixlan is different from remote in terms of speed, operational, and is_rs_peer.
|
||||
There is no local-ixf existing.
|
||||
|
||||
We need to send out notifications to net and ix
|
||||
|
||||
Routerserver attribute missing from remote, we ignore it
|
||||
"""
|
||||
data = setup_test_data("ixf.member.5")
|
||||
network = entities["net"]["UPDATE_DISABLED"]
|
||||
ixlan = entities["ixlan"][0]
|
||||
entities["netixlan"].append(
|
||||
NetworkIXLan.objects.create(
|
||||
network=network,
|
||||
ixlan=ixlan,
|
||||
asn=network.asn,
|
||||
speed=20000,
|
||||
ipaddr4="195.69.147.250",
|
||||
ipaddr6="2001:7f8:1::a500:2906:1",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
operational=False,
|
||||
)
|
||||
)
|
||||
|
||||
importer = ixf.Importer()
|
||||
|
||||
if not save:
|
||||
return assert_idempotent(importer, ixlan, data, save=False)
|
||||
|
||||
importer.update(ixlan, data=data)
|
||||
importer.notify_proposals()
|
||||
|
||||
assert NetworkIXLan.objects.last().is_rs_peer == True
|
||||
assert IXFMemberData.objects.first().is_rs_peer == None
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data, save=save)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_netixlan(entities, use_ip, save):
|
||||
"""
|
||||
@@ -403,7 +493,7 @@ def test_add_netixlan(entities, use_ip, save):
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
assert NetworkIXLan.objects.count() == 1
|
||||
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# test rollback
|
||||
import_log = IXLanIXFMemberImportLog.objects.first()
|
||||
@@ -413,6 +503,32 @@ def test_add_netixlan(entities, use_ip, save):
|
||||
assert NetworkIXLan.objects.first().ipaddr6 == None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_netixlan_no_routeserver(entities, use_ip, save):
|
||||
"""
|
||||
No NetIXLan exists but remote IXF data has information
|
||||
to create one (without conflicts). Updates are enabled
|
||||
so we create the NetIXLan.
|
||||
|
||||
routeserver attribute isnt present at remote ,we ignore it
|
||||
"""
|
||||
data = setup_test_data("ixf.member.4")
|
||||
network = entities["net"]["UPDATE_ENABLED"]
|
||||
ixlan = entities["ixlan"][0]
|
||||
|
||||
importer = ixf.Importer()
|
||||
|
||||
if not save:
|
||||
return assert_idempotent(importer, ixlan, data, save=False)
|
||||
|
||||
importer.update(ixlan, data=data)
|
||||
importer.notify_proposals()
|
||||
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
assert NetworkIXLan.objects.count() == 1
|
||||
assert NetworkIXLan.objects.first().is_rs_peer == False
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_add_netixlan_conflict_local_ixf(entities, use_ip, save):
|
||||
"""
|
||||
@@ -480,7 +596,7 @@ def test_add_netixlan_conflict_local_ixf(entities, use_ip, save):
|
||||
# irrelevant
|
||||
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
assert_idempotent(importer, ixlan, data, save=save)
|
||||
|
||||
elif (network.ipv4_support and not use_ip(4)) or (
|
||||
@@ -495,7 +611,7 @@ def test_add_netixlan_conflict_local_ixf(entities, use_ip, save):
|
||||
email_info = [
|
||||
("CREATE", network.asn, "195.69.147.250", "2001:7f8:1::a500:2906:1",)
|
||||
]
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
assert NetworkIXLan.objects.count() == 0
|
||||
@@ -514,13 +630,8 @@ def test_add_netixlan_conflict_local_ixf(entities, use_ip, save):
|
||||
else:
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
assert NetworkIXLan.objects.count() == 0
|
||||
assert_no_emails()
|
||||
|
||||
# assert (
|
||||
# "IPv4 195.69.147.250 does not match any prefix on this ixlan"
|
||||
# in ixfmemberdata.error
|
||||
# )
|
||||
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
assert_idempotent(importer, ixlan, data, save=save)
|
||||
|
||||
|
||||
@@ -581,7 +692,7 @@ def test_add_netixlan_conflict(entities, save):
|
||||
# #771
|
||||
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data, save=save)
|
||||
@@ -692,7 +803,7 @@ def test_suggest_add_local_ixf(entities, use_ip, save):
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
assert NetworkIXLan.objects.count() == 1
|
||||
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data, save=save)
|
||||
@@ -900,7 +1011,7 @@ def test_suggest_add_no_netixlan_local_ixf(entities, use_ip, save):
|
||||
assert_network_email(network, email_info)
|
||||
|
||||
else:
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -943,7 +1054,11 @@ def test_suggest_add_no_netixlan(entities, use_ip, save):
|
||||
email_info = [("CREATE", network.asn, None, "2001:7f8:1::a500:2906:1")]
|
||||
|
||||
assert_network_email(network, email_info)
|
||||
assert_no_ix_email(ixlan.ix)
|
||||
|
||||
if network.ipv4_support and network.ipv6_support:
|
||||
assert_no_ix_email(ixlan.ix)
|
||||
else:
|
||||
assert_protocol_conflict_email(network, ix=ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -1019,7 +1134,7 @@ def test_single_ipaddr_matches(entities, save):
|
||||
assert importer.log["data"][1]["action"] == "delete"
|
||||
assert importer.log["data"][2]["action"] == "add"
|
||||
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -1061,7 +1176,7 @@ def test_single_ipaddr_matches_no_auto_update(entities, use_ip, save):
|
||||
importer.notify_proposals()
|
||||
|
||||
if use_ip(4) and use_ip(6):
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
assert IXFMemberData.objects.count() == 0
|
||||
assert NetworkIXLan.objects.count() == 1
|
||||
|
||||
@@ -1072,7 +1187,7 @@ def test_single_ipaddr_matches_no_auto_update(entities, use_ip, save):
|
||||
):
|
||||
|
||||
assert len(importer.log["data"]) == 0
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
else:
|
||||
|
||||
@@ -1269,7 +1384,7 @@ def test_delete(entities, save):
|
||||
|
||||
assert log["action"] == "delete"
|
||||
assert NetworkIXLan.objects.filter(status="ok").count() == 1
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -1345,7 +1460,7 @@ def test_suggest_delete_local_ixf_has_flag(entities, save):
|
||||
assert NetworkIXLan.objects.count() == 2
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
|
||||
assert_no_emails()
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -1527,7 +1642,7 @@ def test_mark_invalid_remote_w_local_ixf_auto_update(entities, save):
|
||||
ipaddr4="195.69.147.200",
|
||||
ipaddr6="2001:7f8:1::a500:2906:2",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
is_rs_peer=False,
|
||||
operational=True,
|
||||
)
|
||||
)
|
||||
@@ -1567,8 +1682,12 @@ def test_mark_invalid_remote_w_local_ixf_auto_update(entities, save):
|
||||
importer.update(ixlan, data=data)
|
||||
importer.notify_proposals()
|
||||
|
||||
assert IXFMemberData.objects.count() == 2
|
||||
assert_no_emails()
|
||||
# #793 count should be 2 if we were not ignoring changes
|
||||
# to is_rs_peer and speed, but because we currently are
|
||||
# one of the pre-existing ixfmemberdata entries gets resolved
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -1598,7 +1717,7 @@ def test_mark_invalid_remote_auto_update(entities, save):
|
||||
ipaddr4="195.69.147.200",
|
||||
ipaddr6="2001:7f8:1::a500:2906:2",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
is_rs_peer=False,
|
||||
operational=True,
|
||||
)
|
||||
)
|
||||
@@ -1613,23 +1732,26 @@ def test_mark_invalid_remote_auto_update(entities, save):
|
||||
importer.notify_proposals()
|
||||
|
||||
assert NetworkIXLan.objects.count() == 1
|
||||
assert IXFMemberData.objects.count() == 2
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
|
||||
# We email to say there is invalid data
|
||||
if network.ipv4_support and network.ipv6_support:
|
||||
email_info = [
|
||||
("CREATE", network.asn, "195.69.147.100", "2001:7f8:1::a500:2906:4"),
|
||||
("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
# #793 no modifies to speed or is_rs_peer for now
|
||||
# ("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
]
|
||||
elif network.ipv4_support:
|
||||
email_info = [
|
||||
("CREATE", network.asn, "195.69.147.100", None),
|
||||
("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
# #793 no modifies to speed or is_rs_peer for now
|
||||
# ("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
]
|
||||
elif network.ipv6_support:
|
||||
email_info = [
|
||||
("CREATE", network.asn, None, "2001:7f8:1::a500:2906:4"),
|
||||
("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
# #793 no modifies to speed or is_rs_peer for now
|
||||
# ("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
]
|
||||
|
||||
assert_ix_email(ixlan.ix, email_info)
|
||||
@@ -1670,6 +1792,9 @@ def test_mark_invalid_remote_w_local_ixf_no_auto_update(entities, save):
|
||||
)
|
||||
)
|
||||
|
||||
# this will get resolved since invalid speed means no changes
|
||||
# to the existing netixlan, thus it becomes noop (#792)
|
||||
|
||||
preexisting_ixfmember_data = IXFMemberData.objects.create(
|
||||
asn=1001,
|
||||
ipaddr4="195.69.147.200",
|
||||
@@ -1683,6 +1808,10 @@ def test_mark_invalid_remote_w_local_ixf_no_auto_update(entities, save):
|
||||
error=json.dumps({"speed": "Invalid speed value: this is not valid"}),
|
||||
)
|
||||
|
||||
# this suggests adding a new netixlan, and will be made
|
||||
# but with an error note attached that the speed could
|
||||
# not be parsed (#792)
|
||||
|
||||
preexisting_ixfmember_data = IXFMemberData.objects.create(
|
||||
asn=1001,
|
||||
ipaddr4="195.69.147.100",
|
||||
@@ -1705,8 +1834,14 @@ def test_mark_invalid_remote_w_local_ixf_no_auto_update(entities, save):
|
||||
importer.update(ixlan, data=data)
|
||||
importer.notify_proposals()
|
||||
|
||||
assert IXFMemberData.objects.count() == 2
|
||||
assert_no_emails()
|
||||
# for email in IXFImportEmail.objects.all():
|
||||
# print(email.message)
|
||||
|
||||
for ixf_member in IXFMemberData.objects.all():
|
||||
print(ixf_member, ixf_member.id)
|
||||
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
assert_no_emails(network, ixlan.ix)
|
||||
|
||||
# Test idempotent
|
||||
assert_idempotent(importer, ixlan, data)
|
||||
@@ -1722,7 +1857,7 @@ def test_mark_invalid_remote_no_auto_update(entities, save):
|
||||
Email the ix
|
||||
"""
|
||||
|
||||
data = setup_test_data("ixf.member.invalid.1")
|
||||
data = setup_test_data("ixf.member.invalid.2")
|
||||
network = entities["net"]["UPDATE_DISABLED"]
|
||||
ixlan = entities["ixlan"][0]
|
||||
|
||||
@@ -1750,27 +1885,31 @@ def test_mark_invalid_remote_no_auto_update(entities, save):
|
||||
importer.update(ixlan, data=data)
|
||||
importer.notify_proposals()
|
||||
|
||||
assert IXFMemberData.objects.count() == 2
|
||||
assert IXFMemberData.objects.count() == 1
|
||||
|
||||
# We send an email about the updates
|
||||
# But it also contains information about the invalid speed
|
||||
if network.ipv4_support and network.ipv6_support:
|
||||
email_info = [
|
||||
("CREATE", network.asn, "195.69.147.100", "2001:7f8:1::a500:2906:4"),
|
||||
("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
# #793 no modifies to speed or is_rs_peer for now
|
||||
# ("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
]
|
||||
elif network.ipv4_support:
|
||||
email_info = [
|
||||
("CREATE", network.asn, "195.69.147.100", None),
|
||||
("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
# #793 no modifies to speed or is_rs_peer for now
|
||||
# ("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
]
|
||||
elif network.ipv6_support:
|
||||
email_info = [
|
||||
("CREATE", network.asn, None, "2001:7f8:1::a500:2906:4"),
|
||||
("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
# #793 no modifies to speed or is_rs_peer for now
|
||||
# ("MODIFY", network.asn, "195.69.147.200", "2001:7f8:1::a500:2906:2"),
|
||||
]
|
||||
|
||||
assert_ix_email(ixlan.ix, email_info)
|
||||
|
||||
assert (
|
||||
"Invalid speed value: This is invalid" in IXFImportEmail.objects.first().message
|
||||
)
|
||||
@@ -1839,19 +1978,23 @@ def test_create_deskpro_tickets_after_x_days(entities):
|
||||
network = entities["net"]["UPDATE_DISABLED"]
|
||||
ixlan = entities["ixlan"][0]
|
||||
|
||||
# disable while #793 is active
|
||||
"""
|
||||
entities["netixlan"].append(
|
||||
NetworkIXLan.objects.create(
|
||||
network=network,
|
||||
ixlan=ixlan,
|
||||
asn=network.asn,
|
||||
speed=10000,
|
||||
ipaddr4="195.69.147.250",
|
||||
ipaddr6="2001:7f8:1::a500:2906:1",
|
||||
ipaddr4="195.69.147.252",
|
||||
ipaddr6="2001:7f8:1::a500:2906:2",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
operational=True,
|
||||
)
|
||||
)
|
||||
"""
|
||||
|
||||
entities["netixlan"].append(
|
||||
NetworkIXLan.objects.create(
|
||||
network=network,
|
||||
@@ -1875,6 +2018,7 @@ def test_create_deskpro_tickets_after_x_days(entities):
|
||||
datetime.timezone.utc
|
||||
) - datetime.timedelta(days=14)
|
||||
ixfmd.save()
|
||||
print(ixfmd.ixf_id, ixfmd.action)
|
||||
|
||||
importer.update(ixlan, data=data)
|
||||
|
||||
@@ -1915,8 +2059,8 @@ def test_create_deskpro_tickets_no_contacts(entities):
|
||||
ixlan=ixlan,
|
||||
asn=network.asn,
|
||||
speed=10000,
|
||||
ipaddr4="195.69.147.250",
|
||||
ipaddr6="2001:7f8:1::a500:2906:1",
|
||||
ipaddr4="195.69.147.251",
|
||||
ipaddr6="2001:7f8:1::a500:2906:2",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
operational=True,
|
||||
@@ -1940,7 +2084,10 @@ def test_create_deskpro_tickets_no_contacts(entities):
|
||||
importer.notify_proposals()
|
||||
|
||||
# Assert Tickets are created immediately
|
||||
assert DeskProTicket.objects.count() == 4
|
||||
if network.ipv6_support:
|
||||
assert DeskProTicket.objects.count() == 4
|
||||
else:
|
||||
assert DeskProTicket.objects.count() == 3
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@@ -1949,19 +2096,6 @@ def test_resolve_deskpro_ticket(entities):
|
||||
network = entities["net"]["UPDATE_DISABLED"]
|
||||
ixlan = entities["ixlan"][0]
|
||||
|
||||
entities["netixlan"].append(
|
||||
NetworkIXLan.objects.create(
|
||||
network=network,
|
||||
ixlan=ixlan,
|
||||
asn=network.asn,
|
||||
speed=20000,
|
||||
ipaddr4="195.69.147.250",
|
||||
ipaddr6="2001:7f8:1::a500:2906:1",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
operational=True,
|
||||
)
|
||||
)
|
||||
importer = ixf.Importer()
|
||||
importer.update(ixlan, data=data)
|
||||
importer.notify_proposals()
|
||||
@@ -1998,7 +2132,10 @@ def test_resolve_deskpro_ticket(entities):
|
||||
# 4 emails total
|
||||
# 2 emails for initial consolidated notification
|
||||
# 2 emails for ticket
|
||||
assert IXFImportEmail.objects.count() == 4
|
||||
if network.ipv4_support and network.ipv6_support:
|
||||
assert IXFImportEmail.objects.count() == 3
|
||||
else:
|
||||
assert IXFImportEmail.objects.count() == 4
|
||||
conflict_emails = IXFImportEmail.objects.filter(subject__icontains="conflict")
|
||||
consolid_emails = IXFImportEmail.objects.exclude(subject__icontains="conflict")
|
||||
assert conflict_emails.count() == 2
|
||||
@@ -2012,14 +2149,23 @@ def test_resolve_deskpro_ticket(entities):
|
||||
if not network.ipv6_support:
|
||||
assert "IX-F data provides IPv6 addresses" in email.message
|
||||
|
||||
|
||||
for email in conflict_emails:
|
||||
assert ticket.deskpro_ref in email.subject
|
||||
|
||||
# Resolve issue
|
||||
netixlan = entities["netixlan"][0]
|
||||
netixlan.speed = 10000
|
||||
netixlan.save()
|
||||
entities["netixlan"].append(
|
||||
NetworkIXLan.objects.create(
|
||||
network=network,
|
||||
ixlan=ixlan,
|
||||
asn=network.asn,
|
||||
speed=10000,
|
||||
ipaddr4="195.69.147.250",
|
||||
ipaddr6="2001:7f8:1::a500:2906:1",
|
||||
status="ok",
|
||||
is_rs_peer=True,
|
||||
operational=True,
|
||||
)
|
||||
)
|
||||
|
||||
# Re run import to notify resolution
|
||||
importer.notifications = []
|
||||
@@ -2327,16 +2473,69 @@ def assert_no_ticket_exists():
|
||||
assert DeskProTicket.objects.count() == 0
|
||||
|
||||
|
||||
def assert_no_emails():
|
||||
assert IXFImportEmail.objects.count() == 0
|
||||
def assert_no_emails(network=None, ix=None):
|
||||
if network and (not network.ipv4_support or not network.ipv6_support):
|
||||
assert_protocol_conflict_email(network, ix=ix, network=network)
|
||||
else:
|
||||
assert IXFImportEmail.objects.count() == 0
|
||||
|
||||
|
||||
def assert_no_ix_email(ix):
|
||||
assert IXFImportEmail.objects.filter(ix=ix.id).count() == 0
|
||||
|
||||
|
||||
def assert_protocol_conflict_email(protocols, ix=None, network=None, solo=True):
|
||||
|
||||
"""
|
||||
Here we assert that protocol conflict notifications go out
|
||||
|
||||
protocols should be the network instance that defines the protocol
|
||||
support
|
||||
|
||||
if ix is set we assert the notification exists for the ix
|
||||
if network is set we assert the notification exists for the network
|
||||
if solo is True we assert that this is the only notification that exists
|
||||
"""
|
||||
|
||||
if not protocols.ipv4_support:
|
||||
unsupported = 4
|
||||
elif not protocols.ipv6_support:
|
||||
unsupported = 6
|
||||
else:
|
||||
raise Exception("Both protocols appear supported")
|
||||
|
||||
search = f"data provides IPv{unsupported} addresses for some "
|
||||
|
||||
if network:
|
||||
qset = IXFImportEmail.objects.filter(net=network)
|
||||
if solo:
|
||||
assert qset.count() == 1
|
||||
assert qset.filter(message__contains=search).count() == 1
|
||||
assert not qset.filter(message__contains="CREATE").exists()
|
||||
assert not qset.filter(message__contains="MODIFY").exists()
|
||||
assert not qset.filter(message__contains="REMOVE").exists()
|
||||
else:
|
||||
assert qset.filter(message__contains=search).exists
|
||||
|
||||
if ix:
|
||||
qset = IXFImportEmail.objects.filter(ix=ix)
|
||||
if solo:
|
||||
assert qset.count() == 1
|
||||
assert qset.filter(message__contains=search).count() == 1
|
||||
assert not qset.filter(message__contains="CREATE").exists()
|
||||
assert not qset.filter(message__contains="MODIFY").exists()
|
||||
assert not qset.filter(message__contains="REMOVE").exists()
|
||||
else:
|
||||
assert qset.filter(message__contains=search).exists
|
||||
|
||||
|
||||
def assert_no_network_email(network):
|
||||
assert IXFImportEmail.objects.filter(net=network.id).count() == 0
|
||||
if network.ipv4_support and network.ipv6_support:
|
||||
assert IXFImportEmail.objects.filter(net=network.id).count() == 0
|
||||
else:
|
||||
assert_protocol_conflict_email(
|
||||
protocols=network, network=network,
|
||||
)
|
||||
|
||||
|
||||
def ticket_list():
|
||||
|
Reference in New Issue
Block a user