1
0
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:
Matt Griswold
2020-08-03 14:51:11 -05:00
committed by GitHub
parent 8e831f54df
commit 0e2829d82f
15 changed files with 888 additions and 110 deletions

View File

@@ -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(

View File

@@ -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(

View File

@@ -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}")

View 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'),
),
]

View 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),
),
]

View File

@@ -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

View File

@@ -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()
},

View File

@@ -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 %}

View File

@@ -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 %}

View File

@@ -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 %}

View 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"
]
}
}
]
}
]
}
]
}

View 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"
]
}
}
]
}
]
}
]
}

View 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"
]
}
}
]
}
]
}
]
}

View 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

View File

@@ -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():