diff --git a/config/facsimile/peeringdb.yaml b/config/facsimile/peeringdb.yaml index 395023db..52815a24 100644 --- a/config/facsimile/peeringdb.yaml +++ b/config/facsimile/peeringdb.yaml @@ -160,6 +160,7 @@ install: - $SRC_DIR$/peeringdb_server/management/commands/pdb_process_admin_tool_command.py - $SRC_DIR$/peeringdb_server/management/commands/pdb_load_data.py - $SRC_DIR$/peeringdb_server/management/commands/pdb_fix_status_history.py + - $SRC_DIR$/peeringdb_server/management/commands/pdb_migrate_ixlans.py - $SRC_DIR$/peeringdb_server/migrations/__init__.py - $SRC_DIR$/peeringdb_server/migrations/0001_initial.py - $SRC_DIR$/peeringdb_server/migrations/0002_partnernship_model.py diff --git a/peeringdb_server/admin.py b/peeringdb_server/admin.py index d9d6c4f9..42d90436 100644 --- a/peeringdb_server/admin.py +++ b/peeringdb_server/admin.py @@ -10,7 +10,7 @@ import django.urls from django.conf.urls import url from django.shortcuts import redirect, Http404 from django.contrib.contenttypes.models import ContentType -from django.contrib import admin +from django.contrib import admin, messages from django.contrib.auth import forms from django.contrib.admin import helpers from django.contrib.admin.actions import delete_selected @@ -296,6 +296,15 @@ def soft_delete(modeladmin, request, queryset): if request.user: reversion.set_user(request.user) + if queryset.model.handleref.tag == "ixlan": + messages.error( + request, + _( + "Ixlans can no longer be directly deleted as they are now synced to the parent exchange" + ), + ) + return + for row in queryset: row.delete() @@ -406,7 +415,13 @@ class IXLanInline(SanitizedAdmin, admin.StackedInline): extra = 0 form = StatusForm exclude = ["arp_sponge"] - readonly_fields = ["ixf_import_attempt_info", "prefixes"] + readonly_fields = ["id", "ixf_import_attempt_info", "prefixes"] + + def has_add_permission(self, request): + return False + + def has_delete_permission(self, request, obj): + return False def ixf_import_attempt_info(self, obj): if obj.ixf_import_attempt: @@ -553,6 +568,7 @@ class IXLanAdminForm(StatusForm): class IXLanAdmin(SoftDeleteAdmin): + actions = [] list_display = ("ix", "name", "descr", "status") search_fields = ("name", "ix__name") list_filter = (StatusFilter,) diff --git a/peeringdb_server/autocomplete_views.py b/peeringdb_server/autocomplete_views.py index 9a64bfe2..0d8f356d 100644 --- a/peeringdb_server/autocomplete_views.py +++ b/peeringdb_server/autocomplete_views.py @@ -131,13 +131,12 @@ class IXLanAutocomplete(AutocompleteHTMLResponse): def get_result_label(self, item): return ( - '
%s
%s
%s
%s
' + '
%s
%s
%s
' % ( item.pk, html.escape(item.ix.name), html.escape(item.ix.country.code), html.escape(item.ix.name_long), - html.escape(item.name), ) ) diff --git a/peeringdb_server/client_adaptor/backend.py b/peeringdb_server/client_adaptor/backend.py index 3a68dfb5..2b7575d0 100644 --- a/peeringdb_server/client_adaptor/backend.py +++ b/peeringdb_server/client_adaptor/backend.py @@ -119,7 +119,10 @@ class Backend(BaseBackend): obj.clean() def save(self, obj): - obj.save() + if obj.HandleRef.tag == "ix": + obj.save(create_ixlan=False) + else: + obj.save() def detect_uniqueness_error(self, exc): """ diff --git a/peeringdb_server/management/commands/pdb_api_test.py b/peeringdb_server/management/commands/pdb_api_test.py index 6a5de782..5039c4d5 100644 --- a/peeringdb_server/management/commands/pdb_api_test.py +++ b/peeringdb_server/management/commands/pdb_api_test.py @@ -346,6 +346,7 @@ class TestJSON(unittest.TestCase): def make_data_ixlan(self, **kwargs): data = { "ix_id": 1, + "id": 1, "name": self.make_name("Test"), "descr": NOTE, "mtu": 12345, @@ -353,6 +354,8 @@ class TestJSON(unittest.TestCase): "rs_asn": 12345, "arp_sponge": None, } + if "ix_id" in kwargs: + data["id"] = kwargs.get("ix_id") data.update(**kwargs) return data @@ -651,7 +654,8 @@ class TestJSON(unittest.TestCase): kwargs_s = {"%s_%s" % (rel, qfld): getattr(SHARED["%s_r_ok" % rel], fld)} kwargs_m = {"%s_%s__in" % (rel, qfld): ",".join([str(id) for id in ids])} - if hasattr(REFTAG_MAP[target], "%s" % rel): + attr = getattr(REFTAG_MAP[target], rel, None) + if attr and not isinstance(attr, property): valid_s = [ r.id @@ -998,6 +1002,11 @@ class TestJSON(unittest.TestCase): SHARED["ix_id"] = r_data.get("id") + # make sure ixlan was created and has matching id + ix = InternetExchange.objects.get(id=SHARED["ix_id"]) + assert ix.ixlan + assert ix.ixlan.id == ix.id + self.assert_update( self.db_org_admin, "ix", @@ -1330,23 +1339,23 @@ class TestJSON(unittest.TestCase): def test_org_admin_002_POST_PUT_DELETE_ixlan(self): data = self.make_data_ixlan(ix_id=SHARED["ix_rw_ok"].id) - r_data = self.assert_create( - self.db_org_admin, - "ixlan", - data, - test_failures={ - "invalid": {"ix_id": ""}, - "perms": {"ix_id": SHARED["ix_r_ok"].id}, - "status": {"ix_id": SHARED["ix_rw_pending"].id}, - }, - ) - - SHARED["ixlan_id"] = r_data["id"] + with self.assertRaises(Exception) as exc: + r_data = self.assert_create( + self.db_org_admin, + "ixlan", + data, + test_failures={ + "invalid": {"ix_id": ""}, + "perms": {"ix_id": SHARED["ix_r_ok"].id}, + "status": {"ix_id": SHARED["ix_rw_pending"].id}, + }, + ) + self.assertIn('Method "POST" not allowed', str(exc.exception)) self.assert_update( self.db_org_admin, "ixlan", - SHARED["ixlan_id"], + SHARED["ixlan_rw_ok"].id, {"name": self.make_name("Test")}, test_failures={ "invalid": {"mtu": "NEEDS TO BE INT"}, @@ -1354,12 +1363,14 @@ class TestJSON(unittest.TestCase): }, ) - self.assert_delete( - self.db_org_admin, - "ixlan", - test_success=SHARED["ixlan_id"], - test_failure=SHARED["ixlan_r_ok"].id, - ) + with self.assertRaises(Exception) as exc: + self.assert_delete( + self.db_org_admin, + "ixlan", + test_success=SHARED["ixlan_rw_ok"].id, + test_failure=SHARED["ixlan_r_ok"].id, + ) + self.assertIn('Method "DELETE" not allowed', str(exc.exception)) ########################################################################## @@ -2082,11 +2093,8 @@ class TestJSON(unittest.TestCase): for i in range(0, 2) ] - # create ixlan at each exchange - ixlans = [ - IXLan.objects.create(status="ok", **self.make_data_ixlan(ix_id=ix.id)) - for ix in exchanges - ] + # collect ixlans + ixlans = [ix.ixlan for ix in exchanges] # all three networks peer at first exchange for net in networks: @@ -2591,13 +2599,15 @@ class TestJSON(unittest.TestCase): def test_readonly_users_002_POST_ixlan(self): for db in self.readonly_dbs(): - self.assert_create( - db, - "ixlan", - self.make_data_ixlan(), - test_failures={"perms": {}}, - test_success=False, - ) + with self.assertRaises(Exception) as exc: + self.assert_create( + db, + "ixlan", + self.make_data_ixlan(), + test_failures={"perms": {}}, + test_success=False, + ) + self.assertIn('Method "POST" not allowed', str(exc.exception)) ########################################################################## @@ -2616,9 +2626,14 @@ class TestJSON(unittest.TestCase): def test_readonly_users_004_DELETE_ixlan(self): for db in self.readonly_dbs(): - self.assert_delete( - db, "ixlan", test_success=False, test_failure=SHARED["ixlan_r_ok"].id - ) + with self.assertRaises(Exception) as exc: + self.assert_delete( + db, + "ixlan", + test_success=False, + test_failure=SHARED["ixlan_r_ok"].id, + ) + self.assertIn('Method "DELETE" not allowed', str(exc.exception)) ########################################################################## @@ -2712,16 +2727,6 @@ class TestJSON(unittest.TestCase): test_failures={"perms": {"net_id": SHARED["net_rw2_ok"].id}}, ) - # user with create perms should not be able to create an ixlan under - # net_rw_ix - self.assert_create( - self.db_crud_create, - "ixlan", - self.make_data_ixlan(ix_id=SHARED["ix_rw3_ok"].id), - test_failures={"perms": {}}, - test_success=False, - ) - # other crud test users should not be able to create a new poc under # net_rw3_ok for p in ["delete", "update"]: @@ -3134,7 +3139,9 @@ class Command(BaseCommand): for k in unset: if k in data: del data[k] - obj = model.objects.create(**data) + obj = model(**data) + obj.save() + cls.log( "%s with status '%s' for %s testing created! (%s)" % (tag.upper(), status, prefix.upper(), obj.updated) @@ -3299,12 +3306,12 @@ class Command(BaseCommand): for status in ["ok", "pending"]: for prefix in ["r", "rw"]: - cls.create_entity( - IXLan, - status=status, - prefix=prefix, - ix_id=SHARED["ix_%s_%s" % (prefix, status)].id, - ) + SHARED["ixlan_{}_{}".format(prefix, status)] = SHARED[ + "ix_{}_{}".format(prefix, status) + ].ixlan + + for status in ["ok", "pending"]: + for prefix in ["r", "rw"]: cls.create_entity( IXLanPrefix, status=status, diff --git a/peeringdb_server/management/commands/pdb_generate_test_data.py b/peeringdb_server/management/commands/pdb_generate_test_data.py index 9fb56308..f9d4a267 100644 --- a/peeringdb_server/management/commands/pdb_generate_test_data.py +++ b/peeringdb_server/management/commands/pdb_generate_test_data.py @@ -58,7 +58,6 @@ class Command(BaseCommand): "net", "ix", "fac", - "ixlan", "ixpfx", "ixfac", "netixlan", @@ -104,6 +103,8 @@ class Command(BaseCommand): params.update(protocol="IPv6") entity = self.mock.create(reftag, **params) self.entities[reftag].append(entity) + elif reftag == "ix": + self.entities["ixlan"].append(entity.ixlan) self.entities["net"].append(self.mock.create("net")) self.entities["ix"].append(self.mock.create("ix")) diff --git a/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py b/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py index adbb95ff..dacfca19 100644 --- a/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py +++ b/peeringdb_server/migrations/0022_ixlan_remove_auto_increment.py @@ -12,6 +12,9 @@ class Migration(migrations.Migration): ] operations = [ - # this change was reverted, but we will keep this empty migration - # so it does not break the migration chain + migrations.AlterField( + model_name="ixlan", + name="id", + field=models.IntegerField(primary_key=True, serialize=False), + ), ] diff --git a/peeringdb_server/mock.py b/peeringdb_server/mock.py index f496b80f..2cab404b 100644 --- a/peeringdb_server/mock.py +++ b/peeringdb_server/mock.py @@ -96,6 +96,10 @@ class Mock(object): # these we don't care about if field.name in ["id", "logo", "version", "created", "updated"]: continue + # if reftag == "ixlan" and field.name != "id": + # continue + # elif reftag != "ixlan": + # continue # this we dont care about either if field.name.find("geocode") == 0: @@ -140,7 +144,15 @@ class Mock(object): # with the same name as the field name else: data[field.name] = getattr(self, field.name)(data, reftag=reftag) - return model.objects.create(**data) + obj = model(**data) + obj.clean() + obj.save() + return obj + + def id(self, data, reftag=None): + if reftag == "ixlan": + return data["ix"].id + return None def status(self, data, reftag=None): return "ok" diff --git a/peeringdb_server/models.py b/peeringdb_server/models.py index 1634c3c2..7b6bf7e3 100644 --- a/peeringdb_server/models.py +++ b/peeringdb_server/models.py @@ -1335,6 +1335,17 @@ class InternetExchange(pdb_models.InternetExchangeBase): ix_id, ) + @property + def ixlan(self): + """ + Returns the ixlan for this exchange + + As per #21 each exchange will get one ixlan with a matching + id, but the schema is to remain unchanged until a major + version bump. + """ + return self.ixlan_set.first() + @property def networks(self): """ @@ -1422,6 +1433,29 @@ class InternetExchange(pdb_models.InternetExchangeBase): ixpfx.status = "ok" ixpfx.save() + def save(self, create_ixlan=True, **kwargs): + """ + When an internet exchange is saved, make sure the ixlan for it + exists + + Keyword Argument(s): + + - create_ixlan (`bool`=True): if True and the ix is missing + it's ixlan, create it + """ + r = super(InternetExchange, self).save(**kwargs) + + if not self.ixlan and create_ixlan: + ixlan = IXLan(ix=self, status=self.status, mtu=0) + + # ixlan id will be set to match ix id in ixlan's clean() + # call + ixlan.clean() + + ixlan.save() + + return r + def validate_phonenumbers(self): self.tech_phone = validate_phonenumber(self.tech_phone, self.country.code) self.policy_phone = validate_phonenumber(self.policy_phone, self.country.code) @@ -1478,6 +1512,11 @@ class IXLan(pdb_models.IXLanBase): Describes a LAN at an exchange """ + # as we are preparing to drop IXLans from the schema, as an interim + # step (#21) we are giving each ix one ixlan with matching ids, so we need + # to have an id field that doesnt automatically increment + id = models.IntegerField(primary_key=True) + ix = models.ForeignKey( InternetExchange, on_delete=models.CASCADE, default=0, related_name="ixlan_set" ) @@ -1498,7 +1537,7 @@ class IXLan(pdb_models.IXLanBase): """ Returns a descriptive label of the ixlan for logging purposes """ - return "ixlan{} {} {}".format(self.id, self.name, self.ix.name) + return "ixlan{} {}".format(self.id, self.ix.name) @classmethod def nsp_namespace_from_id(cls, org_id, ix_id, id): @@ -1566,6 +1605,27 @@ class IXLan(pdb_models.IXLanBase): return True return False + def clean(self): + # id is set and does not match the parent ix id + + if self.id and self.id != self.ix.id: + raise ValidationError({"id": _("IXLan id needs to match parent ix id")}) + + # id is not set (new ixlan) + + if not self.id: + + # ixlan for ix already exists + + if self.ix.ixlan: + raise ValidationError(_("Ixlan for exchange already exists")) + + # enforce correct id moving forward + + self.id = self.ix.id + + return super(IXLan, self).clean() + @reversion.create_revision() def add_netixlan(self, netixlan_info, save=True, save_others=True): """ diff --git a/peeringdb_server/rest.py b/peeringdb_server/rest.py index b633ba08..8fd7ddc7 100644 --- a/peeringdb_server/rest.py +++ b/peeringdb_server/rest.py @@ -558,7 +558,7 @@ def ref_dict(): return {tag: view.model for tag, view, na in router.registry} -def model_view_set(model): +def model_view_set(model, methods=None): """ shortcut for peeringdb models to generate viewset and register in the API urls """ @@ -593,6 +593,9 @@ def model_view_set(model): # create the type viewset_t = type(model + "ViewSet", (ModelViewSet,), clsdict) + if methods: + viewset_t.http_method_names = methods + # register with the rest router for incoming requests ref_tag = model_t.handleref.tag router.register(ref_tag, viewset_t, basename=ref_tag) @@ -603,7 +606,7 @@ def model_view_set(model): FacilityViewSet = model_view_set("Facility") InternetExchangeViewSet = model_view_set("InternetExchange") InternetExchangeFacilityViewSet = model_view_set("InternetExchangeFacility") -IXLanViewSet = model_view_set("IXLan") +IXLanViewSet = model_view_set("IXLan", methods=["get", "put"]) IXLanPrefixViewSet = model_view_set("IXLanPrefix") NetworkViewSet = model_view_set("Network") NetworkContactViewSet = model_view_set("NetworkContact") diff --git a/peeringdb_server/serializers.py b/peeringdb_server/serializers.py index 11453ce5..76180086 100644 --- a/peeringdb_server/serializers.py +++ b/peeringdb_server/serializers.py @@ -2005,8 +2005,13 @@ class InternetExchangeSerializer(ModelSerializer): # create ix r = super(InternetExchangeSerializer, self).create(validated_data) + ixlan = r.ixlan + # create ixlan - ixlan = IXLan.objects.create(name="Main", ix=r, status="pending") + # if False:# not ixlan: + # ixlan = IXLan(ix=r, status="pending") + # ixlan.clean() + # ixlan.save() # see if prefix already exists in a deleted state ixpfx = IXLanPrefix.objects.filter(prefix=prefix, status="deleted").first() diff --git a/peeringdb_server/static/20c/twentyc.edit.js b/peeringdb_server/static/20c/twentyc.edit.js index 91588a7d..d7a4d2f2 100644 --- a/peeringdb_server/static/20c/twentyc.edit.js +++ b/peeringdb_server/static/20c/twentyc.edit.js @@ -174,7 +174,7 @@ twentyc.editable.action.register( modules = [], targets = 1, changed, - status={"error":false}, + status={"error":false, "data":{}}, i; @@ -182,13 +182,16 @@ twentyc.editable.action.register( targets--; if(error) status.error = true; + + if(data) { + $.extend(status.data, data); + } + if(!targets) { if(!status.error && !me.noToggle) { - if(data) - container.editable("toggle", { data:data }); - else - container.editable("toggle"); + container.editable("toggle", { data:status.data }); } + /* if(!status.error && container.data("edit-always")) { // if container is always toggled to edit mode @@ -212,6 +215,8 @@ twentyc.editable.action.register( var target = twentyc.editable.target.instantiate(container); changed = target.data._changed; + $.extend(status.data, target.data); + // prepare modules container.find("[data-edit-module]"). //editable("filter", { belongs : container }). @@ -244,7 +249,14 @@ twentyc.editable.action.register( } var grouped = container.editable("filter", { grouped : true }).not("[data-edit-module]"); - targets += grouped.length; + + grouped.each(function(idx) { + var target = twentyc.editable.target.instantiate($(this)); + $.extend(status.data, target.data); + if(target.data._changed) { + targets += 1 + } + }); if(changed || container.data("edit-always-submit") == "yes"){ $(target).on("success", function(ev, data) { @@ -258,8 +270,9 @@ twentyc.editable.action.register( // submit main target var result = target.execute(); - } else + } else { dec_targets({}, {}); + } // submit grouped targets diff --git a/peeringdb_server/static/peeringdb.js b/peeringdb_server/static/peeringdb.js index 98c495f3..edf0df70 100644 --- a/peeringdb_server/static/peeringdb.js +++ b/peeringdb_server/static/peeringdb.js @@ -252,6 +252,13 @@ PeeringDB.ViewActions = { actions : {} } +PeeringDB.ViewActions.actions.ix_ixf_preview = function(netId) { + $("#ixf-preview-modal").modal("show"); + var preview = new PeeringDB.IXFPreview() + preview.request(netId, $("#ixf-log")); +} + + PeeringDB.ViewActions.actions.net_ixf_preview = function(netId) { $("#ixf-preview-modal").modal("show"); var preview = new PeeringDB.IXFNetPreview() @@ -262,7 +269,6 @@ PeeringDB.ViewActions.actions.net_ixf_postmortem = function(netId) { $("#ixf-postmortem-modal").modal("show"); var postmortem = new PeeringDB.IXFNetPostmortem() postmortem.request(netId, $("#ixf-postmortem")); - } @@ -1087,6 +1093,7 @@ twentyc.editable.target.register( "base" ); + /* * editable api listing module */ @@ -1345,34 +1352,6 @@ twentyc.editable.module.register( }, - // FINALIZERS: IXLAN - - finalize_add_ixlan : function(data, callback, sentData) { - - // we currently do not publish ix-f setting fields on the API - // so we need to set those from sent data - data.ixf_ixp_member_list_url = sentData.ixf_ixp_member_list_url; - data.ixf_ixp_import_enabled = sentData.ixf_ixp_import_enabled; - callback(data); - }, - - - finalize_row_ixlan : function(rowId, row, data) { - row.editable("payload", { - ix_id : data.ix_id - }) - row.data("edit-label", gettext("IXLAN") + ": "+data.name); /// - - var modPrefix = row.find('[data-edit-module="api_listing"]'); - modPrefix.editable("sync"); - modPrefix.editable("toggle"); - - var cmpPrefixAdd = row.find('[data-edit-component="add"]') - cmpPrefixAdd.editable("payload", { - ixlan_id : data.id - }); - }, - // FINALIZERS: IXLAN PREFIX finalize_row_ixpfx : function(rowId, row, data) { diff --git a/peeringdb_server/static/site.css b/peeringdb_server/static/site.css index 132c6b10..8f9ac0e3 100644 --- a/peeringdb_server/static/site.css +++ b/peeringdb_server/static/site.css @@ -660,6 +660,7 @@ table.result { div.list { margin-left: 15px; margin-right: 15px; + margin-top: 15px; } div.list div.header { @@ -680,6 +681,7 @@ div.list h5 { overflow: hidden; text-overflow: ellipsis; text-align: left; + margin-left: -11px; } div.list div.empty-result { diff --git a/peeringdb_server/templates/site/view.html b/peeringdb_server/templates/site/view.html index 364f4b35..8db1d231 100644 --- a/peeringdb_server/templates/site/view.html +++ b/peeringdb_server/templates/site/view.html @@ -63,7 +63,27 @@
{% for row in data.fields %} - {% if not row.value|dont_render %} + {% if row.type == "group" %} +
+
+
{{ row.label }}
+
+ +
+ {% for payload_row in row.payload %} +
{{ payload_row.value }}
+ {% endfor %} +
+ + {% elif row.type == "group_end" %} +
+ {% endif %} + + {% if not row.value|dont_render and row.type != "group" and row.type != "group_end" %} {% if not row.admin or permissions.can_write %}
diff --git a/peeringdb_server/templates/site/view_exchange_assets.html b/peeringdb_server/templates/site/view_exchange_assets.html index 214b7331..f6da15a3 100644 --- a/peeringdb_server/templates/site/view_exchange_assets.html +++ b/peeringdb_server/templates/site/view_exchange_assets.html @@ -26,84 +26,13 @@
-
-
- {% if permissions.can_delete %} - × - {% endif %} -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - - {% trans "Enable IX-F Import" %} -
- -
-
- -
- -
-
- -
-
-
-
-
- -
-
-
-
-
-
+
{% if permissions.can_delete %} × {% endif %}
-
+
diff --git a/peeringdb_server/templates/site/view_exchange_bottom.html b/peeringdb_server/templates/site/view_exchange_bottom.html index 7ababeb2..871193d0 100644 --- a/peeringdb_server/templates/site/view_exchange_bottom.html +++ b/peeringdb_server/templates/site/view_exchange_bottom.html @@ -1,244 +1,81 @@ {% load util %} {% load i18n %} - + + -{% if not data.lan_simple_view %}
+ data-edit-target="api:ixpfx"> + +
{% trans "Prefixes" %}
-
-
-
LANs
-
-
- -
-
-
-
{% trans "Name" %}
+
+
{% trans "Protocol" %}
-
-
{% trans "DOT1Q" %}
-
-
-
{% trans "MTU" %}
+
+
{% trans "Prefix" %}
-
+ {% with x=instance.ixlan %} +
-
-
{% trans "No filter matches." %}
-
{% trans "You may filter by" %} {%trans "Name" %} {% trans "or" %} {% trans "dot1q" %}.
-
- - {% for x in data.ixlans %} -
+ {% for prefix in x.ixpfx_set_active_or_pending %} +
-
{{ x.ix_id }}
+
{{ x.id }}
+
{{ prefix.protocol }}
-
+
{% if permissions.can_delete %} - × + × {% endif %} -
- {{ x.name }} -
+ {{ prefix.protocol }}
-
- dot1q_support -
-
-
{{ x.mtu|fallback:"" }}
-
-
-
{{ x.ixf_ixp_member_list_url|fallback:"" }}
- {% if permissions.can_write %} - - {% endif %} -
- {% if permissions.can_write %} -
- - - - {% trans "Enable IX-F Import" %} -
- {% endif %} -
- -
- - {% for prefix in x.ixpfx_set_active_or_pending %} -
-
-
{{ x.id }}
-
{{ prefix.protocol }}
-
-
- {% if permissions.can_delete %} - × - {% endif %} - {{ prefix.protocol }} -
-
{{ prefix.prefix }}
-
- {% endfor %} -
- {% if permissions.can_create %} -
-
-
{{ x.id }}
-
- -
-
-
-
-
- -
-
- - {% endif %} -
+ data-edit-name="prefix" + data-edit-required="yes">{{ prefix.prefix }}
- {% endfor %} -
- {% if permissions.can_create %}
+
+
{{ x.id }}
+
-
-
{{ instance.id }}
-
- -
-
{% trans "Name" %}
-
-
-
-
-
- -
-
{% trans "DOT1Q" %}
-
-
-
-
-
- -
-
{% trans "MTU" %}
-
-
-
-
-
- - -
-
{% trans "IXF Member Export URL" %}
-
-
-
-
-
- - -
-
{% trans "IXF Import Enabled" %}
-
-
-
-
-
- - - - +
+
+
+
+
+ +
- {% endif %} - - - + {% endwith %} +
-{% endif %} -
- - {% endif %} diff --git a/peeringdb_server/views.py b/peeringdb_server/views.py index 11b2e792..8c07f87e 100644 --- a/peeringdb_server/views.py +++ b/peeringdb_server/views.py @@ -1270,48 +1270,57 @@ def view_exchange(request, id): ], } - ixlan_num = data["ixlans"].count() + # IXLAN field group (form) - if ixlan_num < 2 and not perms.get("can_edit"): - # if there is less than one LAN connected to this ix - # we want to render a simplified view to read-only - # viewers + ixlan = exchange.ixlan - data["lan_simple_view"] = True - - if ixlan_num == 1: - - ixlan = data["ixlans"].first() - - data["fields"].extend( - [ - {"type": "sub", "label": _("LAN")}, + data["fields"].extend( + [ + { + "type": "group", + "target": "api:ixlan:update", + "id": ixlan.id, + "label": _("LAN"), + "payload": [{"name": "ix_id", "value": exchange.id},], + }, + { + "type": "flags", + "label": _("DOT1Q"), + "value": [{"name": "dot1q_support", "value": ixlan.dot1q_support}], + }, + { + "type": "number", + "name": "mtu", + "label": _("MTU"), + "value": (ixlan.mtu or 0), + }, + { + "type": "flags", + "label": _("Enable IX-F Import"), + "value": [ { - "type": "number", - "name": "mtu", - "label": _("MTU"), - "value": ixlan.mtu or "", - }, - { - "type": "bool", - "name": "dot1q_support", - "label": _("DOT1Q"), - "value": ixlan.dot1q_support, - }, - ] - ) - - data["fields"].extend( - [ - { - "type": "string", - "name": "prefix_%d" % prefix.id, - "label": _(prefix.protocol), - "value": prefix.prefix, + "name": "ixf_ixp_import_enabled", + "value": ixlan.ixf_ixp_import_enabled, } - for prefix in ixlan.ixpfx_set_active - ] - ) + ], + "admin": True, + }, + { + "type": "url", + "label": _("IX-F Member Export URL"), + "name": "ixf_ixp_member_list_url", + "value": ixlan.ixf_ixp_member_list_url, + "admin": True, + }, + { + "type": "action", + "label": _("IX-F Import Preview"), + "actions": [{"label": _("Preview"), "action": "ixf_preview",},], + "admin": True, + }, + {"type": "group_end"}, + ] + ) return view_component( request, "exchange", data, "Exchange", perms=perms, instance=exchange diff --git a/tests/data/ixf/logs/skip_prefix_mismatch.json b/tests/data/ixf/logs/skip_prefix_mismatch.json index ba8821b7..d5d7bff1 100644 --- a/tests/data/ixf/logs/skip_prefix_mismatch.json +++ b/tests/data/ixf/logs/skip_prefix_mismatch.json @@ -7,8 +7,8 @@ "data": [ { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", "ixlan_id": 2, "asn": 2906 }, @@ -17,113 +17,113 @@ }, { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", "net_id": 1, - "ipaddr4": "195.69.146.250", - "is_rs_peer": true, "speed": 10000, + "ipaddr4": "195.69.146.250", + "ipaddr6": "2001:7f8:1::a500:2906:2", + "is_rs_peer": true, "ixlan_id": 2, - "asn": 2906, - "ipaddr6": "2001:7f8:1::a500:2906:2" + "asn": 2906 }, "action": "ignore", "reason": "Ip addresses (195.69.146.250, 2001:7f8:1::a500:2906:2) do not match any prefix on this ixlan" }, { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", "net_id": 1, - "ipaddr4": "195.69.147.250", - "is_rs_peer": true, "speed": 10000, + "ipaddr4": "195.69.147.250", + "ipaddr6": "2001:7f8:1::a500:2906:1", + "is_rs_peer": true, "ixlan_id": 2, - "asn": 2906, - "ipaddr6": "2001:7f8:1::a500:2906:1" + "asn": 2906 }, "action": "ignore", "reason": "Ip addresses (195.69.147.250, 2001:7f8:1::a500:2906:1) do not match any prefix on this ixlan" }, { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", "net_id": 1, - "ipaddr4": "195.69.147.251", - "is_rs_peer": true, "speed": 10000, + "ipaddr4": "195.69.147.251", + "ipaddr6": "2001:7f8:1::a500:2906:5", + "is_rs_peer": true, "ixlan_id": 2, - "asn": 2906, - "ipaddr6": "2001:7f8:1::a500:2906:5" + "asn": 2906 }, "action": "ignore", "reason": "Ip addresses (195.69.147.251, 2001:7f8:1::a500:2906:5) do not match any prefix on this ixlan" }, { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", "net_id": 1, - "ipaddr4": "195.69.147.253", - "is_rs_peer": true, "speed": 10000, + "ipaddr4": "195.69.147.253", + "ipaddr6": "", + "is_rs_peer": true, "ixlan_id": 2, - "asn": 2906, - "ipaddr6": "" + "asn": 2906 }, "action": "ignore", "reason": "Ip addresses (195.69.147.253, None) do not match any prefix on this ixlan" }, { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", "net_id": 1, - "ipaddr4": "", - "is_rs_peer": true, "speed": 10000, + "ipaddr4": "", + "ipaddr6": "2001:7f8:1::a500:2906:6", + "is_rs_peer": true, "ixlan_id": 2, - "asn": 2906, - "ipaddr6": "2001:7f8:1::a500:2906:6" + "asn": 2906 }, "action": "ignore", "reason": "Ip addresses (None, 2001:7f8:1::a500:2906:6) do not match any prefix on this ixlan" }, { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", "net_id": 1, - "ipaddr4": "195.69.146.251", - "is_rs_peer": true, "speed": 20000, - "ixlan_id": 2, - "asn": 2906, - "ipaddr6": "2001:7f8:1::a500:2906:3" - }, - "action": "noop", - "reason": "Connection is currently marked as inactive" - }, - { - "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, - "net_id": 1, - "ipaddr4": "195.69.146.252", + "ipaddr4": "195.69.146.251", + "ipaddr6": "2001:7f8:1::a500:2906:3", "is_rs_peer": true, - "speed": 10000, "ixlan_id": 2, - "asn": 2906, - "ipaddr6": "2001:7f8:1::a500:2906:4" + "asn": 2906 }, "action": "noop", "reason": "Connection is currently marked as inactive" }, { "peer": { - "ix_name": "Test Exchange", - "ix_id": 1, + "ix_id": 2, + "ix_name": "Test Exchange 2", + "net_id": 1, + "speed": 10000, + "ipaddr4": "195.69.146.252", + "ipaddr6": "2001:7f8:1::a500:2906:4", + "is_rs_peer": true, + "ixlan_id": 2, + "asn": 2906 + }, + "action": "noop", + "reason": "Connection is currently marked as inactive" + }, + { + "peer": { + "ix_id": 2, + "ix_name": "Test Exchange 2", "ixlan_id": 2, "asn": 2906 }, diff --git a/tests/test_admin.py b/tests/test_admin.py index ff072a1f..c702873a 100644 --- a/tests/test_admin.py +++ b/tests/test_admin.py @@ -68,9 +68,7 @@ class AdminTests(TestCase): cls.admin_user.save() # set up some ixlans - cls.entities["ixlan"] = [ - models.IXLan.objects.create(ix=ix, status="ok") for ix in cls.entities["ix"] - ] + cls.entities["ixlan"] = [ix.ixlan for ix in cls.entities["ix"]] # set up a prefix cls.entities["ixpfx"] = [ diff --git a/tests/test_exporters.py b/tests/test_exporters.py index eac50901..89a96d66 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -101,9 +101,7 @@ class AdvancedSearchExportTest(ClientCase): ] # create ixlans - cls.ixlan = [ - IXLan.objects.create(ix=cls.ix[i - 1], status="ok") for i in entity_count - ] + cls.ixlan = [ix.ixlan for ix in cls.ix] # create netixlans cls.netixlan = [ diff --git a/tests/test_ixf_member_import.py b/tests/test_ixf_member_import.py index eb1c9781..de4cf060 100644 --- a/tests/test_ixf_member_import.py +++ b/tests/test_ixf_member_import.py @@ -73,15 +73,17 @@ class JsonMembersListTestCase(ClientCase): cls.entities["ix"] = [ InternetExchange.objects.create( name="Test Exchange", org=cls.entities["org"][0], status="ok" - ) + ), + InternetExchange.objects.create( + name="Test Exchange 2", org=cls.entities["org"][0], status="ok" + ), + InternetExchange.objects.create( + name="Test Exchange 3", org=cls.entities["org"][0], status="ok" + ), ] # create ixlan(s) - cls.entities["ixlan"] = [ - IXLan.objects.create(ix=cls.entities["ix"][0], status="ok"), - IXLan.objects.create(ix=cls.entities["ix"][0], status="ok"), - IXLan.objects.create(ix=cls.entities["ix"][0], status="ok"), - ] + cls.entities["ixlan"] = [ix.ixlan for ix in cls.entities["ix"]] # create ixlan prefix(s) cls.entities["ixpfx"] = [ @@ -677,7 +679,8 @@ class TestImportPreview(ClientCase): name="Test IX", status="ok", org=cls.org ) - cls.ixlan = IXLan.objects.create(status="ok", ix=cls.ix) + cls.ixlan = cls.ix.ixlan + IXLanPrefix.objects.create( ixlan=cls.ixlan, status="ok", prefix="195.69.144.0/22", protocol="IPv4" ) diff --git a/tests/test_undelete.py b/tests/test_undelete.py index 6c75f803..f9672020 100644 --- a/tests/test_undelete.py +++ b/tests/test_undelete.py @@ -103,45 +103,3 @@ class TestUndelete(ClientCase): assert self.ixlan_a.status == "ok" assert self.ixlan_a.netixlan_set_active.count() == 0 - - def test_undelete_ixlan_netixlan_dupe_same_ix(self): - ixlan_c = REFTAG_MAP["ixlan"].objects.create(ix=self.ix_a, status="ok") - netixlan_a = self.ixlan_a.netixlan_set.first() - self.ixlan_a.delete() - netixlan_c = REFTAG_MAP["netixlan"].objects.create( - asn=self.net_a.asn, - ixlan=ixlan_c, - status="ok", - ipaddr4=netixlan_a.ipaddr4, - network=self.net_a, - speed=100, - ) - - assert ixlan_c.netixlan_set_active.count() == 1 - - self._undelete(self.ixlan_a) - - assert self.ixlan_a.status == "ok" - assert self.ixlan_a.netixlan_set_active.count() == 1 - assert ixlan_c.netixlan_set_active.count() == 0 - - def test_undelete_ixlan_netixlan_dupe_same_ix_ipv6(self): - ixlan_c = REFTAG_MAP["ixlan"].objects.create(ix=self.ix_a, status="ok") - netixlan_a = self.ixlan_a.netixlan_set.first() - self.ixlan_a.delete() - netixlan_c = REFTAG_MAP["netixlan"].objects.create( - asn=self.net_a.asn, - ixlan=ixlan_c, - status="ok", - ipaddr6=netixlan_a.ipaddr6, - network=self.net_a, - speed=100, - ) - - assert ixlan_c.netixlan_set_active.count() == 1 - - self._undelete(self.ixlan_a) - - assert self.ixlan_a.status == "ok" - assert self.ixlan_a.netixlan_set_active.count() == 1 - assert ixlan_c.netixlan_set_active.count() == 0 diff --git a/tests/test_validators.py b/tests/test_validators.py index 0c6766de..36a19f7e 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -139,7 +139,7 @@ def test_validate_prefixlen(): def test_validate_prefix_overlap(): org = Organization.objects.create(name="Test org", status="ok") ix = InternetExchange.objects.create(name="Text exchange", status="ok", org=org) - ixlan = IXLan.objects.create(ix=ix, status="ok") + ixlan = ix.ixlan pfx1 = IXLanPrefix.objects.create( ixlan=ixlan,