2018-11-08 19:45:21 +00:00
import re
2020-07-15 02:07:01 -05:00
import json
2018-11-08 19:45:21 +00:00
import datetime
from itertools import chain
import uuid
import ipaddress
import googlemaps . exceptions
import requests
import reversion
2020-01-08 13:29:58 -06:00
import django . urls
2018-11-08 19:45:21 +00:00
from django . contrib . auth . models import AbstractBaseUser , PermissionsMixin
from django . contrib . auth . models import UserManager , Group
from django . contrib . contenttypes . fields import GenericForeignKey
from django . contrib . contenttypes . models import ContentType
2020-01-08 13:29:58 -06:00
from django . core import validators
2018-11-08 19:45:21 +00:00
from django . core . mail . message import EmailMultiAlternatives
from django . core . exceptions import ValidationError
from django . db import models , transaction
from django . utils import timezone
2020-07-15 02:07:01 -05:00
from django . utils . html import strip_tags
2018-11-08 19:45:21 +00:00
from django . utils . http import urlquote
from django . utils . translation import ugettext_lazy as _
from django . utils . translation import override
2020-07-26 23:36:27 -05:00
from django . utils . functional import Promise
2018-11-08 19:45:21 +00:00
from django . conf import settings
from django . template import loader
from django_namespace_perms . util import autodiscover_namespaces , has_perms
from django_handleref . models import (
CreatedDateTimeField ,
UpdatedDateTimeField ,
)
import django_peeringdb . models as pdb_models
2019-12-05 16:57:52 +00:00
from django_inet . models import ASNField
2018-11-08 19:45:21 +00:00
from allauth . account . models import EmailAddress , EmailConfirmation
from allauth . socialaccount . models import SocialAccount
from passlib . hash import sha256_crypt
from peeringdb_server . inet import RdapLookup , RdapNotFoundError
from peeringdb_server . validators import (
2019-12-05 16:57:52 +00:00
validate_address_space ,
validate_info_prefixes4 ,
validate_info_prefixes6 ,
validate_prefix_overlap ,
2020-01-08 13:29:58 -06:00
validate_phonenumber ,
2020-03-31 11:04:26 +00:00
validate_irr_as_set ,
2019-12-05 16:57:52 +00:00
)
2018-11-08 19:45:21 +00:00
2019-12-05 16:57:52 +00:00
SPONSORSHIP_LEVELS = (
( 1 , _ ( " Silver " ) ) ,
( 2 , _ ( " Gold " ) ) ,
( 3 , _ ( " Platinum " ) ) ,
( 4 , _ ( " Diamond " ) ) ,
)
2018-11-08 19:45:21 +00:00
PARTNERSHIP_LEVELS = ( ( 1 , _ ( " Data Validation " ) ) , ( 2 , _ ( " RIR " ) ) )
2019-12-05 16:57:52 +00:00
COMMANDLINE_TOOLS = (
( " pdb_renumber_lans " , _ ( " Renumber IP Space " ) ) ,
( " pdb_fac_merge " , _ ( " Merge Facilities " ) ) ,
( " pdb_fac_merge_undo " , _ ( " Merge Facilities: UNDO " ) ) ,
( " pdb_undelete " , _ ( " Restore Object(s) " ) ) ,
)
2018-11-08 19:45:21 +00:00
2019-01-04 10:02:28 +00:00
if settings . TUTORIAL_MODE :
COMMANDLINE_TOOLS + = ( ( " pdb_wipe " , _ ( " Reset Environment " ) ) , )
2019-09-07 01:08:37 -05:00
COMMANDLINE_TOOLS + = ( ( " pdb_ixf_ixp_member_import " , _ ( " IX-F Import " ) ) , )
2019-12-05 16:57:52 +00:00
2018-11-08 19:45:21 +00:00
def debug_mail ( * args ) :
for arg in list ( args ) :
print ( arg )
print ( " ----------------------------------- " )
def make_relation_filter ( field , filt , value , prefix = None ) :
if prefix :
field = re . sub ( " ^ %s __ " % prefix , " " , field )
field = re . sub ( " ^ %s _ " % prefix , " " , field )
if field == prefix :
field = " id "
if filt :
2020-07-15 02:07:01 -05:00
filt = { f " { field } __ { filt } " : value }
2018-11-08 19:45:21 +00:00
else :
filt = { field : value }
filt . update ( status = " ok " )
return filt
def validate_PUT_ownership ( user , instance , data , fields ) :
"""
Helper function that checks if a user has write perms to
the instance provided as well as write perms to any
child instances specified by fields as they exist on
the model and in data
example :
validate_PUT_ownership (
request . user ,
network_contact ,
{
" network " : 123 ,
. . .
} ,
[ " network " ]
)
will check that the user has write perms to
1. < NetworkContact > network_contact
2. < Network > network_contact . network
3. < Network > network ( id = 123 )
if any fail the permission check False is returned .
"""
if not has_perms ( user , instance , " update " ) :
return False
for fld in fields :
if fld == " net " :
field_name = " network "
elif fld == " fac " :
field_name = " facility "
else :
field_name = fld
a = getattr ( instance , field_name )
try :
s_id = int ( data . get ( fld , data . get ( " %s _id " % fld ) ) )
except ValueError :
continue
if a . id != s_id :
try :
other = a . __class__ . objects . get ( id = s_id )
if not has_perms ( user , other , " update " ) :
return False
except ValueError : # if id is not intable
return False
return True
def is_suggested ( entity ) :
"""
Check if the network , facility or exchange is a suggested
entity ( is it a memeber of the organization designated to
hold suggested entities )
"""
# if no org is specified, entity suggestion is turned
# off
if not getattr ( settings , " SUGGEST_ENTITY_ORG " , 0 ) :
return False
org_id = getattr ( entity , " org_id " , 0 )
return org_id == settings . SUGGEST_ENTITY_ORG
class UTC ( datetime . tzinfo ) :
"""
UTC + 0 tz for tz aware datetime fields
"""
def utcoffset ( self , d ) :
return datetime . timedelta ( seconds = 0 )
class URLField ( pdb_models . URLField ) :
"""
local defaults for URLField
"""
2019-12-05 16:57:52 +00:00
2018-11-08 19:45:21 +00:00
pass
2020-07-15 02:07:01 -05:00
2020-07-26 23:36:27 -05:00
class ValidationErrorEncoder ( json . JSONEncoder ) :
def default ( self , obj ) :
if isinstance ( obj , ValidationError ) :
if hasattr ( obj , " error_dict " ) :
return obj . error_dict
return obj . message
elif isinstance ( obj , Promise ) :
return f " { obj } "
return super ( ) . default ( obj )
2020-06-24 12:55:01 -05:00
class ProtectedAction ( ValueError ) :
2020-07-15 02:07:01 -05:00
def __init__ ( self , obj ) :
super ( ) . __init__ ( obj . not_deletable_reason )
self . protected_object = obj
2020-06-24 12:55:01 -05:00
class ProtectedMixin :
"""
Mixin that implements checks for changing
/ deleting a model instance that will block
such actions under certain circumstances
"""
@property
def deletable ( self ) :
"""
Should return whether the object is currently
in a state where it can safely be soft - deleted
If not not deletable , should specify reason in
` _not_deletable_reason ` property .
If deletable should , set ` _not_deletable_reason `
property to None
"""
return True
@property
def not_deletable_reason ( self ) :
return getattr ( self , " _not_deletable_reason " , None )
def delete ( self , hard = False , force = False ) :
if not self . deletable and not force :
2020-07-15 02:07:01 -05:00
raise ProtectedAction ( self )
2020-06-24 12:55:01 -05:00
self . delete_cleanup ( )
return super ( ) . delete ( hard = hard )
def delete_cleanup ( self ) :
"""
Runs cleanup before delete
Override this in the class that uses this mixin ( if needed )
"""
return
2018-11-08 19:45:21 +00:00
class GeocodeBaseMixin ( models . Model ) :
"""
Mixin to use for geocode enabled entities
Allows an entity to be geocoded with the pdb_geo_sync command
"""
2019-12-05 16:57:52 +00:00
geocode_status = models . BooleanField (
default = False ,
help_text = _ (
" Has this object ' s latitude and longitude been syncronized to it ' s address fields "
) ,
)
2018-11-08 19:45:21 +00:00
geocode_date = models . DateTimeField (
2019-12-05 16:57:52 +00:00
blank = True , null = True , help_text = _ ( " Last time of attempted geocode " )
)
2018-11-08 19:45:21 +00:00
geocode_error = models . TextField (
2019-12-05 16:57:52 +00:00
blank = True , null = True , help_text = _ ( " Error message of previous geocode attempt " )
)
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
class Meta :
2018-11-08 19:45:21 +00:00
abstract = True
@property
def geocode_coordinates ( self ) :
"""
Return a tuple holding the latitude and longitude
"""
if self . latitude is not None and self . longitude is not None :
return ( self . latitude , self . longitude )
return None
@property
def geocode_address ( self ) :
"""
Returns an address string suitable for googlemaps query
"""
2019-12-05 16:57:52 +00:00
# pylint: disable=missing-format-attribute
2020-01-08 13:29:58 -06:00
return " {e.address1} {e.address2} , {e.city} , {e.state} {e.zipcode} " . format (
2019-12-05 16:57:52 +00:00
e = self
)
2018-11-08 19:45:21 +00:00
def geocode ( self , gmaps , save = True ) :
"""
Sets the latitude , longitude field values of this model by geocoding the
address specified in the relevant fields .
Argument ( s ) :
- gmaps : googlemaps instance
"""
try :
2019-12-05 16:57:52 +00:00
result = gmaps . geocode (
self . geocode_address , components = { " country " : self . country . code }
)
if result and (
" street_address " in result [ 0 ] [ " types " ]
or " establishment " in result [ 0 ] [ " types " ]
or " premise " in result [ 0 ] [ " types " ]
or " subpremise " in result [ 0 ] [ " types " ]
) :
2018-11-08 19:45:21 +00:00
loc = result [ 0 ] . get ( " geometry " ) . get ( " location " )
self . latitude = loc . get ( " lat " )
self . longitude = loc . get ( " lng " )
self . geocode_error = None
else :
self . latitude = None
self . longitude = None
self . geocode_error = _ ( " Address not found " )
self . geocode_status = True
return result
2020-01-08 13:29:58 -06:00
except (
googlemaps . exceptions . HTTPError ,
googlemaps . exceptions . ApiError ,
) as inst :
2018-11-08 19:45:21 +00:00
self . geocode_error = str ( inst )
self . geocode_status = True
2020-01-08 13:29:58 -06:00
except googlemaps . exceptions . Timeout as inst :
2018-11-08 19:45:21 +00:00
self . geocode_error = _ ( " API Timeout " )
self . geocode_status = False
finally :
self . geocode_date = datetime . datetime . now ( ) . replace ( tzinfo = UTC ( ) )
if save :
self . save ( )
class UserOrgAffiliationRequest ( models . Model ) :
"""
Whenever a user requests to be affiliated to an Organization
through an ASN the request is stored in this object .
When an ASN is entered that is not in the database yet it will
notify PDB staff
When an ASN is entered that is already in the database the organzation
adminstration is notified and they can then approve or deny
the affiliation request themselves .
Please look at signals . py for the logic of notification as
well as deriving the organization from the ASN during creation .
"""
org = models . ForeignKey (
2019-12-05 16:57:52 +00:00
" peeringdb_server.Organization " ,
2020-01-08 13:29:58 -06:00
on_delete = models . CASCADE ,
2019-12-05 16:57:52 +00:00
null = True ,
blank = True ,
help_text = _ (
" This organization in our database that was derived from the provided ASN or organization name. If this is empty it means no matching organization was found. "
) ,
related_name = " affiliation_requests " ,
)
2018-11-08 19:45:21 +00:00
org_name = models . CharField (
2019-12-05 16:57:52 +00:00
max_length = 255 ,
null = True ,
blank = True ,
help_text = _ ( " The organization name entered by the user " ) ,
)
2018-11-08 19:45:21 +00:00
2019-12-05 16:57:52 +00:00
asn = ASNField ( help_text = _ ( " The ASN entered by the user " ) , null = True , blank = True )
2018-11-08 19:45:21 +00:00
2019-12-05 16:57:52 +00:00
user = models . ForeignKey (
" peeringdb_server.User " ,
2020-01-08 13:29:58 -06:00
on_delete = models . CASCADE ,
2019-12-05 16:57:52 +00:00
help_text = _ ( " The user that made the request " ) ,
related_name = " affiliation_requests " ,
)
2018-11-08 19:45:21 +00:00
created = CreatedDateTimeField ( )
status = models . CharField (
2019-12-05 16:57:52 +00:00
max_length = 254 ,
choices = [
( " pending " , _ ( " Pending " ) ) ,
( " approved " , _ ( " Approved " ) ) ,
( " denied " , _ ( " Denied " ) ) ,
2020-04-01 11:08:12 +00:00
( " canceled " , _ ( " Canceled " ) ) ,
2019-12-05 16:57:52 +00:00
] ,
help_text = _ ( " Status of this request " ) ,
)
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
class Meta :
2018-11-08 19:45:21 +00:00
db_table = " peeringdb_user_org_affil_request "
verbose_name = _ ( " User to Organization Affiliation Request " )
verbose_name_plural = _ ( " User to Organization Affiliation Requests " )
@property
def name ( self ) :
"""
If org is set , returns the org ' s name otherwise returns the
value specified in self . org_name
"""
if self . org_id :
return self . org . name
elif self . org_name :
return self . org_name
return self . asn
def approve ( self ) :
"""
approve request and add user to org ' s usergroup
"""
if self . org_id :
2019-12-05 16:57:52 +00:00
if (
self . org . admin_usergroup . user_set . count ( ) > 0
or self . org . usergroup . user_set . count ( ) > 0
) :
2018-11-08 19:45:21 +00:00
# if there are other users in this org, add user as normal
# member
self . org . usergroup . user_set . add ( self . user )
else :
# if there are no other users in this org, add user as admin
# member
self . org . admin_usergroup . user_set . add ( self . user )
# we set user to verified
2020-06-24 12:55:01 -05:00
if not self . user . is_verified_user :
2018-11-08 19:45:21 +00:00
self . user . set_verified ( )
# since it was approved, we dont need to keep the
# request item around
self . status = " approved "
self . delete ( )
def deny ( self ) :
"""
deny request , marks request as denied and keeps
it around until requesting user deletes it
"""
self . status = " denied "
self . save ( )
2020-04-01 11:08:12 +00:00
def cancel ( self ) :
"""
deny request , marks request as canceled and keeps
it around until requesting user deletes it
"""
self . status = " canceled "
self . save ( )
2018-11-08 19:45:21 +00:00
def notify_ownership_approved ( self ) :
"""
Sends a notification email to the requesting user
"""
if not self . org :
return
2019-12-05 16:57:52 +00:00
# FIXME: why not have the `override` call in email_user in the first place?
2018-11-08 19:45:21 +00:00
with override ( self . user . locale ) :
self . user . email_user (
2020-01-08 13:29:58 -06:00
_ ( ' Your affiliation to Organization " {} " has been approved ' ) . format (
2019-12-05 16:57:52 +00:00
self . org . name
) ,
2018-11-08 19:45:21 +00:00
loader . get_template (
2019-12-05 16:57:52 +00:00
" email/notify-user-uoar-ownership-approved.txt "
) . render (
{
2018-11-08 19:45:21 +00:00
" uoar " : self ,
2020-07-15 02:07:01 -05:00
" org_url " : f " { settings . BASE_URL } /org/ { self . org . id } " ,
2019-12-05 16:57:52 +00:00
" support_email " : settings . DEFAULT_FROM_EMAIL ,
}
) ,
)
2018-11-08 19:45:21 +00:00
class VerificationQueueItem ( models . Model ) :
"""
Keeps track of new items created that need to be reviewed and approved
by administrators
Queue items are added through the create signals tied to the various
objects ( peeringdb_server / signals . py )
"""
# reference to the item that requires review stored in generic fk
2020-01-08 13:29:58 -06:00
content_type = models . ForeignKey ( ContentType , on_delete = models . CASCADE )
2018-11-08 19:45:21 +00:00
object_id = models . PositiveIntegerField ( )
2019-12-05 16:57:52 +00:00
item = GenericForeignKey ( " content_type " , " object_id " )
2018-11-08 19:45:21 +00:00
user = models . ForeignKey (
2019-12-05 16:57:52 +00:00
" peeringdb_server.User " ,
2020-01-08 13:29:58 -06:00
on_delete = models . CASCADE ,
2019-12-05 16:57:52 +00:00
related_name = " vqitems " ,
null = True ,
blank = True ,
help_text = _ ( " The item that this queue is attached to was created by this user " ) ,
)
2018-11-08 19:45:21 +00:00
created = CreatedDateTimeField ( )
notified = models . BooleanField ( default = False )
2020-07-15 02:07:01 -05:00
class Meta :
2018-11-08 19:45:21 +00:00
db_table = " peeringdb_verification_queue "
2019-11-16 21:32:27 -06:00
unique_together = ( ( " content_type " , " object_id " ) , )
2018-11-08 19:45:21 +00:00
@classmethod
def get_for_entity ( cls , entity ) :
"""
Returns verification queue item for the provided
entity if it exists , else raises a DoesNotExist
exception
"""
return cls . objects . get (
content_type = ContentType . objects . get_for_model ( type ( entity ) ) ,
2019-12-05 16:57:52 +00:00
object_id = entity . id ,
)
2018-11-08 19:45:21 +00:00
@property
def item_admin_url ( self ) :
"""
Return admin url for the object in the verification queue
"""
2020-01-08 13:29:58 -06:00
return django . urls . reverse (
2019-12-05 16:57:52 +00:00
" admin: %s _ %s _change "
% ( self . content_type . app_label , self . content_type . model ) ,
args = ( self . object_id , ) ,
)
2018-11-08 19:45:21 +00:00
@property
def approve_admin_url ( self ) :
"""
Return admin url for approval of the verification queue item
"""
2020-01-08 13:29:58 -06:00
return django . urls . reverse (
2020-07-15 02:07:01 -05:00
f " admin: { self . _meta . app_label } _ { self . _meta . model_name } _actions " ,
2019-12-05 16:57:52 +00:00
args = ( self . id , " vq_approve " ) ,
)
2018-11-08 19:45:21 +00:00
@property
def deny_admin_url ( self ) :
"""
Return admin url for denial of the verification queue item
"""
2020-01-08 13:29:58 -06:00
return django . urls . reverse (
2020-07-15 02:07:01 -05:00
f " admin: { self . _meta . app_label } _ { self . _meta . model_name } _actions " ,
2019-12-05 16:57:52 +00:00
args = ( self . id , " vq_deny " ) ,
)
2018-11-08 19:45:21 +00:00
2019-11-16 21:33:01 -06:00
@reversion.create_revision ( )
2018-11-08 19:45:21 +00:00
def approve ( self ) :
"""
Approve the verification queue item
"""
if hasattr ( self . item , " status " ) :
self . item . status = " ok "
if hasattr ( self . item , " vq_approve " ) :
self . item . vq_approve ( )
self . item . save ( )
def deny ( self ) :
"""
Deny the verification queue item
"""
if hasattr ( self . item , " vq_deny " ) :
self . item . vq_deny ( )
else :
if hasattr ( self . item , " ref_tag " ) :
self . item . delete ( hard = True )
else :
self . item . delete ( )
class DeskProTicket ( models . Model ) :
subject = models . CharField ( max_length = 255 )
body = models . TextField ( )
2020-01-08 13:29:58 -06:00
user = models . ForeignKey ( " peeringdb_server.User " , on_delete = models . CASCADE )
2018-11-08 19:45:21 +00:00
created = models . DateTimeField ( auto_now_add = True )
2020-07-26 23:36:27 -05:00
published = models . DateTimeField ( null = True , blank = True )
deskpro_ref = models . CharField (
max_length = 32 ,
null = True ,
blank = True ,
help_text = _ ( " Ticket reference on the DeskPRO side " ) ,
)
deskpro_id = models . IntegerField (
null = True , blank = True , help_text = _ ( " Ticket id on the DeskPRO side " )
)
2018-11-08 19:45:21 +00:00
2020-04-13 08:07:09 +00:00
class Meta :
verbose_name = _ ( " DeskPRO Ticket " )
verbose_name_plural = _ ( " DeskPRO Tickets " )
2018-11-08 19:45:21 +00:00
@reversion.register
2020-06-24 12:55:01 -05:00
class Organization ( ProtectedMixin , pdb_models . OrganizationBase ) :
2018-11-08 19:45:21 +00:00
"""
Describes a peeringdb organization
"""
# FIXME: change this to ImageField - keep
# FileField for now as the server doesn't have all the
# dependencies installedd (libjpeg / Pillow)
logo = models . FileField (
2019-12-05 16:57:52 +00:00
upload_to = " logos/ " ,
null = True ,
blank = True ,
help_text = _ (
" Allows you to upload and set a logo image file for this organization "
) ,
)
2018-11-08 19:45:21 +00:00
@staticmethod
def autocomplete_search_fields ( ) :
return (
" id__iexact " ,
" name__icontains " ,
)
def __unicode__ ( self ) :
return self . name
2020-04-10 15:00:27 +00:00
def related_label ( self ) :
"""
Used by grappelli autocomplete for representation
Since grappelli doesnt easily allow us to filter status
during autocomplete lookup , we make sure the objects
are marked accordingly in the result
"""
if self . status == " deleted " :
2020-07-15 02:07:01 -05:00
return f " [DELETED] { self } "
return f " { self } "
2020-04-10 15:00:27 +00:00
2020-01-08 13:29:58 -06:00
@property
def search_result_name ( self ) :
"""
This will be the name displayed for quick search matches
of this entity
"""
return self . name
2018-11-08 19:45:21 +00:00
@property
def admin_url ( self ) :
"""
Return the admin url for this organization ( in / cp )
"""
2020-01-08 13:29:58 -06:00
return django . urls . reverse (
2019-12-05 16:57:52 +00:00
" admin:peeringdb_server_organization_change " , args = ( self . id , )
)
2018-11-08 19:45:21 +00:00
2020-04-03 13:36:44 +00:00
@property
def view_url ( self ) :
"""
Return the URL to this organizations web view
"""
return " {} {} " . format (
settings . BASE_URL , django . urls . reverse ( " org-view " , args = ( self . id , ) )
)
2020-06-24 12:55:01 -05:00
@property
def deletable ( self ) :
"""
Returns whether or not the organization is currently
in a state where it can be marked as deleted .
This will be False for organization ' s of which ANY
of the following is True :
- has a network under it with status = ok
- has a facility under it with status = ok
- has an exchange under it with status = ok
"""
is_empty = (
2020-07-15 02:07:01 -05:00
self . ix_set_active . count ( ) == 0
and self . fac_set_active . count ( ) == 0
and self . net_set_active . count ( ) == 0
2020-06-24 12:55:01 -05:00
)
if not is_empty :
2020-07-15 02:07:01 -05:00
self . _not_deletable_reason = _ ( " Organization has active objects under it. " )
2020-06-24 12:55:01 -05:00
return False
elif self . sponsorship and self . sponsorship . active :
self . _not_deletable_reason = _ (
2020-07-15 02:07:01 -05:00
" Organization is currently an active sponsor. "
" Please contact PeeringDB support to help facilitate "
2020-06-24 12:55:01 -05:00
" the removal of this organization. "
)
return False
else :
self . _not_deletable_reason = None
return True
2018-11-08 19:45:21 +00:00
@property
def owned ( self ) :
"""
Returns whether or not the organization has been claimed
by any users
"""
return self . admin_usergroup . user_set . count ( ) > 0
@property
def rdap_collect ( self ) :
"""
Fetche rdap results for all networks under this org and returns
them by asn
"""
r = { }
for net in self . net_set_active :
try :
rdap = RdapLookup ( ) . get_asn ( net . asn )
if rdap :
r [ net . asn ] = rdap
2020-01-08 13:29:58 -06:00
except RdapNotFoundError as inst :
2018-11-08 19:45:21 +00:00
pass
return r
@property
def urls ( self ) :
"""
Returns all the websites for the org based on it ' s
website field and the website fields on all the entities it
owns
"""
rv = [ ]
if self . website :
rv . append ( self . website )
for tag in [ " ix " , " net " , " fac " ] :
for ent in getattr ( self , " %s _set_active " % tag ) :
if ent . website :
rv . append ( ent . website )
return list ( set ( rv ) )
@property
def nsp_namespace_manage ( self ) :
"""
Org administrators need CRUD to this namespace in order
to execute administrative actions ( user management , user permission
management )
"""
return " peeringdb.manage_organization. %s " % self . id
@classmethod
def nsp_namespace_from_id ( cls , id ) :
return " peeringdb.organization. %s " % id
@property
def pending_affiliations ( self ) :
"""
Returns queryset holding pending affiliations to this
organization
"""
return self . affiliation_requests . filter ( status = " pending " )
@property
def net_set_active ( self ) :
"""
Returns queryset holding active networks in this organization
"""
return self . net_set ( manager = " handleref " ) . filter ( status = " ok " )
@property
def fac_set_active ( self ) :
"""
Returns queryset holding active facilities in this organization
"""
return self . fac_set ( manager = " handleref " ) . filter ( status = " ok " )
@property
def ix_set_active ( self ) :
"""
Returns queryset holding active exchanges in this organization
"""
return self . ix_set ( manager = " handleref " ) . filter ( status = " ok " )
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this organization
"""
return self . __class__ . nsp_namespace_from_id ( self . id )
@property
def group_name ( self ) :
"""
Returns usergroup name for this organization
"""
return " org. %s " % self . id
@property
def admin_group_name ( self ) :
"""
Returns admin usergroup name for this organization
"""
return " %s .admin " % self . group_name
@property
def usergroup ( self ) :
"""
Returns the usergroup for this organization
"""
return Group . objects . get ( name = self . group_name )
@property
def admin_usergroup ( self ) :
"""
Returns the admin usergroup for this organization
"""
return Group . objects . get ( name = self . admin_group_name )
@property
def all_users ( self ) :
"""
returns a set of all users in the org ' s user and admin groups
"""
users = { }
for user in self . usergroup . user_set . all ( ) :
users [ user . id ] = user
for user in self . admin_usergroup . user_set . all ( ) :
users [ user . id ] = user
2020-01-08 13:29:58 -06:00
return sorted ( list ( users . values ( ) ) , key = lambda x : x . full_name )
2018-11-08 19:45:21 +00:00
@property
def nsp_ruleset ( self ) :
"""
Returns a dict containing rules for django namespace perms
to be used when applying perms to serialized oranization
data
"""
return {
# since poc are stored in a list we need to specify a list
# handler for it, its a class function on NetworkContact that
# returns a relative permission namespace for each poc in the
# list
" list-handlers " : {
2019-12-05 16:57:52 +00:00
" poc_set " : { " namespace " : NetworkContact . nsp_namespace_in_list }
2018-11-08 19:45:21 +00:00
}
}
@property
def sponsorship ( self ) :
"""
Returns sponsorship object for this organization . If the organization
has no sponsorship ongoing return None
"""
now = datetime . datetime . now ( ) . replace ( tzinfo = UTC ( ) )
2019-12-05 16:57:52 +00:00
return (
self . sponsorship_set . filter ( start_date__lte = now , end_date__gte = now )
. order_by ( " -start_date " )
. first ( )
)
2018-11-08 19:45:21 +00:00
@classmethod
@reversion.create_revision ( )
def create_from_rdap ( cls , rdap , asn , org_name = None ) :
"""
Creates organization from rdap result object
"""
name = rdap . org_name
if not name :
name = org_name or ( " AS %d " % ( asn ) )
if cls . objects . filter ( name = name ) . exists ( ) :
return cls . objects . get ( name = name ) , False
else :
org = cls . objects . create ( name = name , status = " ok " )
return org , True
2020-06-24 12:55:01 -05:00
def delete_cleanup ( self , hard = False ) :
for affiliation in self . affiliation_requests . filter ( status = " pending " ) :
affiliation . cancel ( )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
2018-11-08 19:45:21 +00:00
def default_time_s ( ) :
"""
Returns datetime set to today with a time of 00 : 00 : 00
"""
now = datetime . datetime . now ( )
return now . replace ( hour = 0 , minute = 0 , second = 0 , tzinfo = UTC ( ) )
def default_time_e ( ) :
"""
Returns datetime set to today with a time of 23 : 59 : 59
"""
now = datetime . datetime . now ( )
return now . replace ( hour = 23 , minute = 59 , second = 59 , tzinfo = UTC ( ) )
class Sponsorship ( models . Model ) :
"""
Allows an organization to be marked for sponsorship
for a designated timespan
"""
2019-12-05 16:57:52 +00:00
orgs = models . ManyToManyField (
Organization ,
through = " peeringdb_server.SponsorshipOrganization " ,
related_name = " sponsorship_set " ,
)
2018-11-08 19:45:21 +00:00
start_date = models . DateTimeField (
2019-12-05 16:57:52 +00:00
_ ( " Sponsorship starts on " ) , default = default_time_s
)
end_date = models . DateTimeField ( _ ( " Sponsorship ends on " ) , default = default_time_e )
2018-11-08 19:45:21 +00:00
notify_date = models . DateTimeField (
2019-12-05 16:57:52 +00:00
_ ( " Expiration notification sent on " ) , null = True , blank = True
)
2018-11-08 19:45:21 +00:00
level = models . PositiveIntegerField ( choices = SPONSORSHIP_LEVELS , default = 1 )
class Meta :
db_table = " peeringdb_sponsorship "
verbose_name = _ ( " Sponsorship " )
verbose_name_plural = _ ( " Sponsorships " )
2019-11-16 21:32:27 -06:00
@classmethod
def active_by_org ( cls ) :
"""
Yields ( Organization , Sponsorship ) for all currently
active sponsorships
"""
now = datetime . datetime . now ( ) . replace ( tzinfo = UTC ( ) )
qset = cls . objects . filter ( start_date__lte = now , end_date__gte = now )
qset = qset . prefetch_related ( " sponsorshiporg_set " )
for sponsorship in qset :
for org in sponsorship . orgs . all ( ) :
yield org , sponsorship
2020-06-24 12:55:01 -05:00
@property
def active ( self ) :
now = datetime . datetime . now ( ) . replace ( tzinfo = UTC ( ) )
2020-07-15 02:07:01 -05:00
return self . start_date < = now and self . end_date > = now
2020-06-24 12:55:01 -05:00
2018-11-08 19:45:21 +00:00
@property
def label ( self ) :
"""
Returns the label for this sponsorship ' s level
"""
return dict ( SPONSORSHIP_LEVELS ) . get ( self . level )
def notify_expiration ( self ) :
"""
Sends an expiration notice to SPONSORSHIPS_EMAIL
Notification is only sent if notify_date < expiration_date
"""
if self . notify_date is not None and self . notify_date > = self . end_date :
return False
msg = loader . get_template (
2019-12-05 16:57:52 +00:00
" email/notify-sponsorship-admin-expiration.txt "
) . render ( { " instance " : self } )
2018-11-08 19:45:21 +00:00
2019-11-16 21:32:27 -06:00
org_names = " , " . join ( [ org . name for org in self . orgs . all ( ) ] )
2019-12-05 16:57:52 +00:00
mail = EmailMultiAlternatives (
2020-01-08 13:29:58 -06:00
( " {} : {} " ) . format ( _ ( " Sponsorship Expired " ) , org_names ) ,
2019-12-05 16:57:52 +00:00
msg ,
settings . DEFAULT_FROM_EMAIL ,
[ settings . SPONSORSHIPS_EMAIL ] ,
)
2018-11-08 19:45:21 +00:00
mail . attach_alternative ( msg . replace ( " \n " , " <br /> \n " ) , " text/html " )
mail . send ( fail_silently = True )
self . notify_date = datetime . datetime . now ( )
self . save ( )
return True
2019-11-16 21:32:27 -06:00
class SponsorshipOrganization ( models . Model ) :
"""
Describes an organization - > sponsorship relationship
"""
2019-12-05 16:57:52 +00:00
2020-01-08 13:29:58 -06:00
org = models . ForeignKey (
Organization , on_delete = models . CASCADE , related_name = " sponsorshiporg_set "
)
sponsorship = models . ForeignKey (
Sponsorship , on_delete = models . CASCADE , related_name = " sponsorshiporg_set "
)
2019-11-16 21:32:27 -06:00
url = models . URLField (
2019-12-05 16:57:52 +00:00
_ ( " URL " ) ,
help_text = _ (
" If specified clicking the sponsorship will take the user to this location "
) ,
blank = True ,
null = True ,
)
2019-11-16 21:32:27 -06:00
logo = models . FileField (
2019-12-05 16:57:52 +00:00
upload_to = " logos/ " ,
null = True ,
blank = True ,
help_text = _ (
" Allows you to upload and set a logo image file for this sponsorship "
) ,
)
2019-11-16 21:32:27 -06:00
2018-11-08 19:45:21 +00:00
class Partnership ( models . Model ) :
"""
Allows an organization to be marked as a partner
It will appear on the " partners " page
"""
2020-01-08 13:29:58 -06:00
org = models . ForeignKey (
Organization , on_delete = models . CASCADE , related_name = " partnerships "
)
2018-11-08 19:45:21 +00:00
level = models . PositiveIntegerField ( choices = PARTNERSHIP_LEVELS , default = 1 )
url = models . URLField (
2019-12-05 16:57:52 +00:00
_ ( " URL " ) ,
help_text = _ (
" If specified clicking the partnership will take the user to this location "
) ,
blank = True ,
null = True ,
)
2018-11-08 19:45:21 +00:00
logo = models . FileField (
2019-12-05 16:57:52 +00:00
upload_to = " logos/ " ,
null = True ,
blank = True ,
help_text = _ (
" Allows you to upload and set a logo image file for this partnership "
) ,
)
2018-11-08 19:45:21 +00:00
class Meta :
db_table = " peeringdb_partnership "
verbose_name = _ ( " Partnership " )
verbose_name_plural = _ ( " Partnerships " )
@property
def label ( self ) :
return dict ( PARTNERSHIP_LEVELS ) . get ( self . level )
class OrganizationMerge ( models . Model ) :
"""
When an organization is merged into another via admin . merge_organizations
it is logged here , allowing the merge to be undone
"""
2020-01-08 13:29:58 -06:00
from_org = models . ForeignKey (
Organization , on_delete = models . CASCADE , related_name = " merged_to "
)
to_org = models . ForeignKey (
Organization , on_delete = models . CASCADE , related_name = " merged_from "
)
2018-11-08 19:45:21 +00:00
created = models . DateTimeField ( _ ( " Merged on " ) , auto_now_add = True )
class Meta :
db_table = " peeringdb_organization_merge "
verbose_name = _ ( " Organization Merge " )
verbose_name_plural = _ ( " Organization Merges " )
def log_entity ( self , entity , note = " " ) :
"""
mark an entity as moved during this particular merge
entity can be any handleref instance or a User instance
"""
2019-12-05 16:57:52 +00:00
return OrganizationMergeEntity . objects . create (
merge = self , entity = entity , note = note
)
2018-11-08 19:45:21 +00:00
def undo ( self ) :
"""
Undo this merge
"""
# undelete original org
self . from_org . status = " ok "
self . from_org . save ( )
for row in self . entities . all ( ) :
entity = row . entity
tag = getattr ( entity , " ref_tag " , None )
if tag :
# move handleref entity
entity . org = self . from_org
entity . save ( )
else :
# move user entity
group = getattr ( self . from_org , row . note )
group . user_set . add ( entity )
self . to_org . usergroup . user_set . remove ( entity )
self . to_org . admin_usergroup . user_set . remove ( entity )
self . delete ( )
class OrganizationMergeEntity ( models . Model ) :
"""
This holds the entities moved during an
organization merge stored in OrganizationMerge
"""
2020-01-08 13:29:58 -06:00
merge = models . ForeignKey (
OrganizationMerge , on_delete = models . CASCADE , related_name = " entities "
)
content_type = models . ForeignKey ( ContentType , on_delete = models . CASCADE )
2018-11-08 19:45:21 +00:00
object_id = models . PositiveIntegerField ( )
2019-12-05 16:57:52 +00:00
entity = GenericForeignKey ( " content_type " , " object_id " )
2018-11-08 19:45:21 +00:00
note = models . CharField ( max_length = 32 , blank = True , null = True )
class Meta :
db_table = " peeringdb_organization_merge_entity "
verbose_name = _ ( " Organization Merge: Entity " )
verbose_name_plural = _ ( " Organization Merge: Entities " )
@reversion.register
2020-07-15 02:07:01 -05:00
class Facility ( ProtectedMixin , pdb_models . FacilityBase , GeocodeBaseMixin ) :
2018-11-08 19:45:21 +00:00
"""
Describes a peeringdb facility
"""
2020-01-08 13:29:58 -06:00
org = models . ForeignKey (
Organization , on_delete = models . CASCADE , related_name = " fac_set "
)
2018-11-08 19:45:21 +00:00
# FIXME: delete cascade needs to be fixed in django-peeringdb, can remove
# this afterwards
class HandleRef :
tag = " fac "
delete_cascade = [ " ixfac_set " , " netfac_set " ]
class Meta ( pdb_models . FacilityBase . Meta ) :
pass
@staticmethod
def autocomplete_search_fields ( ) :
"""
Returns a tuple of field query strings to be used during quick search
query
"""
return (
" id__iexact " ,
" name__icontains " ,
)
@classmethod
def nsp_namespace_in_list ( cls ) :
"""
Returns the permissioning namespace when a facility
is contained in list
"""
return str ( cls . id )
@classmethod
def nsp_namespace_from_id ( cls , org_id , fac_id ) :
2020-07-26 23:36:27 -05:00
return " {} .facility. {} " . format (
Organization . nsp_namespace_from_id ( org_id ) , fac_id
)
2018-11-08 19:45:21 +00:00
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_net ( cls , value = None , filt = None , field = " network_id " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Returns queryset of Facility objects that
are related to the network specified via net_id
Relationship through netfac - > net
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = NetworkFacility . handleref . filter ( * * filt )
return qset . filter ( id__in = [ i . facility_id for i in q ] )
@classmethod
def related_to_ix ( cls , value = None , filt = None , field = " ix_id " , qset = None ) :
"""
Returns queryset of Facility objects that
are related to the ixwork specified via ix_id
Relationship through ixfac - > ix
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = InternetExchangeFacility . handleref . filter ( * * filt )
return qset . filter ( id__in = [ i . facility_id for i in q ] )
@classmethod
def overlapping_asns ( cls , asns , qset = None ) :
"""
Returns queryset of Facility objects
that have a relationship to all asns specified in ` asns `
Relationship through netfac
Arguments :
- asns < list > : list of asns
Keyword Arguments :
- qset < Facility QuerySet > : if specified use as base query
Returns :
- Facility QuerySet
"""
facilities = { }
shared_facilities = [ ]
count = len ( asns )
if count == 1 :
raise ValidationError ( _ ( " Need to specify at least two asns " ) )
if count > 25 :
raise ValidationError ( _ ( " Can only compare a maximum of 25 asns " ) )
# fist we need to collect all active facilities related to any
# of the specified asns
for asn in asns :
for netfac in NetworkFacility . objects . filter (
2019-12-05 16:57:52 +00:00
network__asn = asn , status = " ok "
) . select_related ( " network " ) :
2018-11-08 19:45:21 +00:00
if netfac . facility_id not in facilities :
facilities [ netfac . facility_id ] = { }
facilities [ netfac . facility_id ] [ asn ] = True
# then we check for the facilities that have all of the asns
# peering by comparing the counts of collected asns at each
# facility with the count of asns provided
2020-01-08 13:29:58 -06:00
for fac_id , collected_asns in list ( facilities . items ( ) ) :
if len ( list ( collected_asns . keys ( ) ) ) == count :
2018-11-08 19:45:21 +00:00
shared_facilities . append ( fac_id )
if not qset :
qset = cls . handleref . undeleted ( )
return qset . filter ( id__in = shared_facilities )
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this facility
"""
return self . __class__ . nsp_namespace_from_id ( self . org_id , self . id )
@property
def sponsorship ( self ) :
"""
Returns sponsorship oject for this facility ( through the owning org )
"""
return self . org . sponsorship
@property
def search_result_name ( self ) :
"""
This will be the name displayed for quick search matches
of this entity
"""
return self . name
@property
def netfac_set_active ( self ) :
"""
Returns queryset of active NetworkFacility ojects connected to this
facility
"""
return self . netfac_set . filter ( status = " ok " )
2019-02-15 17:46:20 +00:00
@property
def ixfac_set_active ( self ) :
"""
Returns queryset of active InternetExchangeFacility objects connected
to this facility
"""
return self . ixfac_set . filter ( status = " ok " )
2018-11-08 19:45:21 +00:00
@property
def net_count ( self ) :
"""
Returns number of Networks at this facility
"""
return self . netfac_set_active . count ( )
2020-04-03 13:36:44 +00:00
@property
def view_url ( self ) :
"""
Return the URL to this facility ' s web view
"""
return " {} {} " . format (
settings . BASE_URL , django . urls . reverse ( " fac-view " , args = ( self . id , ) )
)
2020-07-15 02:07:01 -05:00
@property
def deletable ( self ) :
"""
Returns whether or not the facility is currently
in a state where it can be marked as deleted .
This will be False for facilites of which ANY
of the following is True :
- has a network facility under it with status = ok
- has an exchange facility under it with status = ok
"""
if self . ixfac_set_active . exists ( ) :
facility_names = " , " . join (
[ ixfac . ix . name for ixfac in self . ixfac_set_active . all ( ) [ : 5 ] ]
)
self . _not_deletable_reason = _ (
" Facility has active exchange presence(s): {} ... "
) . format ( facility_names )
return False
elif self . netfac_set_active . exists ( ) :
network_names = " , " . join (
[ netfac . network . name for netfac in self . netfac_set_active . all ( ) [ : 5 ] ]
)
self . _not_deletable_reason = _ (
" Facility has active network presence(s): {} ... "
) . format ( network_names )
return False
else :
self . _not_deletable_reason = None
return True
2020-04-03 13:36:44 +00:00
2018-11-08 19:45:21 +00:00
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " org " ] )
2020-04-20 09:45:48 -05:00
def validate_phonenumbers ( self ) :
self . tech_phone = validate_phonenumber ( self . tech_phone , self . country . code )
self . sales_phone = validate_phonenumber ( self . sales_phone , self . country . code )
2018-11-08 19:45:21 +00:00
@reversion.register
2020-07-15 02:07:01 -05:00
class InternetExchange ( ProtectedMixin , pdb_models . InternetExchangeBase ) :
2018-11-08 19:45:21 +00:00
"""
Describes a peeringdb exchange
"""
2020-01-08 13:29:58 -06:00
org = models . ForeignKey (
Organization , on_delete = models . CASCADE , related_name = " ix_set "
)
2018-11-08 19:45:21 +00:00
@staticmethod
def autocomplete_search_fields ( ) :
"""
Returns a tuple of field query strings to be used during quick search
query
"""
return (
" id__iexact " ,
" name__icontains " ,
)
def __unicode__ ( self ) :
return self . name
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_ixlan ( cls , value = None , filt = None , field = " ixlan_id " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Returns queryset of InternetExchange objects that
are related to IXLan specified by ixlan_id
Relationship through ixlan
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value , prefix = " ixlan " )
q = IXLan . handleref . filter ( * * filt )
return qset . filter ( id__in = [ ix . ix_id for ix in q ] )
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_ixfac ( cls , value = None , filt = None , field = " ixfac_id " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Returns queryset of InternetExchange objects that
are related to IXfac link specified by ixfac_id
Relationship through ixfac
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value , prefix = " ixfac " )
q = InternetExchangeFacility . handleref . filter ( * * filt )
return qset . filter ( id__in = [ ix . ix_id for ix in q ] )
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_fac ( cls , filt = None , value = None , field = " facility_id " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Returns queryset of InternetExchange objects that
are related to the facility specified by fac_id
Relationship through ixfac - > fac
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = InternetExchangeFacility . handleref . filter ( * * filt )
return qset . filter ( id__in = [ ix . ix_id for ix in q ] )
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_net ( cls , filt = None , value = None , field = " network_id " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Returns queryset of InternetExchange objects that
are related to the network specified by network_id
Relationship through netixlan - > ixlan
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = NetworkIXLan . handleref . filter ( * * filt ) . select_related ( " ixlan " )
return qset . filter ( id__in = [ nx . ixlan . ix_id for nx in q ] )
@classmethod
def related_to_ipblock ( cls , ipblock , qset = None ) :
"""
Returns queryset of InternetExchange objects that
have ixlan prefixes matching the ipblock specified
Relationship through ixlan - > ixpfx
"""
if not qset :
qset = cls . handleref . undeleted ( )
q = IXLanPrefix . objects . select_related ( " ixlan " ) . filter (
2019-12-05 16:57:52 +00:00
prefix__startswith = ipblock
)
2018-11-08 19:45:21 +00:00
return qset . filter ( id__in = [ pfx . ixlan . ix_id for pfx in q ] )
@classmethod
def overlapping_asns ( cls , asns , qset = None ) :
"""
Returns queryset of InternetExchange objects
that have a relationship to all asns specified in ` asns `
Relationship through ixlan - > netixlan
Arguments :
- asns < list > : list of asns
Keyword Arguments :
- qset < InternetExchange QuerySet > : if specified use as base query
Returns :
- InternetExchange QuerySet
"""
exchanges = { }
shared_exchanges = [ ]
count = len ( asns )
if count == 1 :
raise ValidationError ( _ ( " Need to specify at least two asns " ) )
if count > 25 :
raise ValidationError ( _ ( " Can only compare a maximum of 25 asns " ) )
# fist we need to collect all active exchanges related to any
# of the specified asns
for asn in asns :
for netixlan in NetworkIXLan . objects . filter (
2019-12-05 16:57:52 +00:00
network__asn = asn , status = " ok "
) . select_related ( " network " , " ixlan " ) :
2018-11-08 19:45:21 +00:00
if netixlan . ixlan . ix_id not in exchanges :
exchanges [ netixlan . ixlan . ix_id ] = { }
exchanges [ netixlan . ixlan . ix_id ] [ asn ] = True
# then we check for the exchanges that have all of the asns
# peering by comparing the counts of collected asns at each
# exchange with the count of asns provided
2020-01-08 13:29:58 -06:00
for ix_id , collected_asns in list ( exchanges . items ( ) ) :
if len ( list ( collected_asns . keys ( ) ) ) == count :
2018-11-08 19:45:21 +00:00
shared_exchanges . append ( ix_id )
if not qset :
qset = cls . handleref . undeleted ( )
return qset . filter ( id__in = shared_exchanges )
2019-05-02 18:03:21 +00:00
@classmethod
def filter_net_count ( cls , filt = None , value = None , qset = None ) :
"""
Filter ix queryset by network count value
Keyword Arguments :
- filt < str > : filter to apply : None , ' lt ' , ' gt ' , ' lte ' , ' gte '
- value < int > : value to filter by
- qset
Returns :
InternetExchange queryset
"""
if not qset :
qset = cls . objects . filter ( status = " ok " )
value = int ( value )
if filt == " lt " :
exchanges = [ ix . id for ix in qset if ix . network_count < value ]
elif filt == " gt " :
exchanges = [ ix . id for ix in qset if ix . network_count > value ]
elif filt == " gte " :
exchanges = [ ix . id for ix in qset if ix . network_count > = value ]
elif filt == " lte " :
exchanges = [ ix . id for ix in qset if ix . network_count < = value ]
else :
exchanges = [ ix . id for ix in qset if ix . network_count == value ]
return qset . filter ( pk__in = exchanges )
2018-11-08 19:45:21 +00:00
@classmethod
def nsp_namespace_in_list ( cls ) :
return str ( cls . id )
@classmethod
def nsp_namespace_from_id ( cls , org_id , ix_id ) :
"""
Returns permissioning namespace for an exchange
"""
2020-07-15 02:07:01 -05:00
return " {} .internetexchange. {} " . format (
2020-07-26 23:36:27 -05:00
Organization . nsp_namespace_from_id ( org_id ) , ix_id ,
2019-12-05 16:57:52 +00:00
)
2018-11-08 19:45:21 +00:00
2020-02-05 21:25:25 -06:00
@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 ( )
2018-11-08 19:45:21 +00:00
@property
def networks ( self ) :
"""
Returns all active networks at this exchange
"""
networks = [ ]
for ixlan in self . ixlan_set_active :
for netixlan in ixlan . netixlan_set_active :
networks . append ( netixlan . network_id )
return list ( set ( networks ) )
@property
def search_result_name ( self ) :
"""
This will be the name displayed for quick search matches
of this entity
"""
return self . name
@property
def network_count ( self ) :
"""
Returns count of networks at this exchange
"""
2019-05-02 18:03:21 +00:00
qset = NetworkIXLan . objects . filter ( ixlan__ix_id = self . id , status = " ok " )
qset = qset . values ( " network_id " ) . annotate ( count = models . Count ( " network_id " ) )
return len ( qset )
2018-11-08 19:45:21 +00:00
@property
def ixlan_set_active ( self ) :
"""
Returns queryset of active ixlan objects at this exchange
"""
return self . ixlan_set ( manager = " handleref " ) . filter ( status = " ok " )
@property
def ixlan_set_active_or_pending ( self ) :
"""
Returns queryset of active or pending ixlan objects at
this exchange
"""
2019-12-05 16:57:52 +00:00
return self . ixlan_set ( manager = " handleref " ) . filter ( status__in = [ " ok " , " pending " ] )
2018-11-08 19:45:21 +00:00
@property
def ixfac_set_active ( self ) :
"""
Returns queryset of active ixfac objects at this exchange
"""
2019-12-05 16:57:52 +00:00
return (
self . ixfac_set ( manager = " handleref " )
. select_related ( " facility " )
. filter ( status = " ok " )
)
2018-11-08 19:45:21 +00:00
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this exchange
"""
return self . __class__ . nsp_namespace_from_id ( self . org_id , self . id )
@property
def sponsorship ( self ) :
"""
Returns sponsorship object for this exchange ( through owning org )
"""
return self . org . sponsorship
2020-04-03 13:36:44 +00:00
@property
def view_url ( self ) :
"""
Return the URL to this facility ' s web view
"""
return " {} {} " . format (
settings . BASE_URL , django . urls . reverse ( " ix-view " , args = ( self . id , ) )
)
2020-07-15 02:07:01 -05:00
@property
def deletable ( self ) :
"""
Returns whether or not the exchange is currently
in a state where it can be marked as deleted .
This will be False for exchanges of which ANY
of the following is True :
- has netixlans connected to it
- ixfac relationship
"""
2020-04-03 13:36:44 +00:00
2020-07-15 02:07:01 -05:00
if self . ixfac_set_active . exists ( ) :
facility_names = " , " . join (
[ ixfac . facility . name for ixfac in self . ixfac_set_active . all ( ) [ : 5 ] ]
)
self . _not_deletable_reason = _ (
" Exchange has active facility connection(s): {} ... "
) . format ( facility_names )
return False
elif self . network_count > 0 :
self . _not_deletable_reason = _ ( " Exchange has active peer(s) " )
return False
else :
self . _not_deletable_reason = None
return True
2020-04-03 13:36:44 +00:00
2018-11-08 19:45:21 +00:00
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " org " ] )
def vq_approve ( self ) :
"""
Called when internet exchange is approved in verification
queue
"""
# since we are creating a pending ixland and prefix
# during exchange creation, we need to make sure those
# get approved as well when the exchange gets approved
for ixlan in self . ixlan_set . filter ( status = " pending " ) :
ixlan . status = " ok "
ixlan . save ( )
for ixpfx in ixlan . ixpfx_set . filter ( status = " pending " ) :
ixpfx . status = " ok "
ixpfx . save ( )
2020-02-05 21:25:25 -06:00
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
"""
2020-07-15 02:07:01 -05:00
r = super ( ) . save ( * * kwargs )
2020-02-05 21:25:25 -06:00
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
2020-01-08 13:29:58 -06:00
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 )
def clean ( self ) :
self . validate_phonenumbers ( )
2018-11-08 19:45:21 +00:00
@reversion.register
class InternetExchangeFacility ( pdb_models . InternetExchangeFacilityBase ) :
"""
Describes facility to exchange relationship
"""
2020-01-08 13:29:58 -06:00
ix = models . ForeignKey (
InternetExchange , on_delete = models . CASCADE , related_name = " ixfac_set "
)
facility = models . ForeignKey (
Facility , on_delete = models . CASCADE , default = 0 , related_name = " ixfac_set "
)
2018-11-08 19:45:21 +00:00
2019-02-15 17:46:20 +00:00
@property
def descriptive_name ( self ) :
"""
Returns a descriptive label of the ixfac for logging purposes
"""
2020-07-15 02:07:01 -05:00
return f " ixfac { self . id } { self . ix . name } <-> { self . facility . name } "
2019-02-15 17:46:20 +00:00
2018-11-08 19:45:21 +00:00
@classmethod
def nsp_namespace_from_id ( cls , org_id , ix_id , id ) :
"""
Returns permissioning namespace for an ixfac
"""
2020-07-26 23:36:27 -05:00
return " {} .fac. {} " . format (
InternetExchange . nsp_namespace_from_id ( org_id , ix_id ) , id
)
2018-11-08 19:45:21 +00:00
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this ixfac
"""
2019-12-05 16:57:52 +00:00
return self . __class__ . nsp_namespace_from_id ( self . ix . org_id , self . ix . id , self . id )
2018-11-08 19:45:21 +00:00
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " ix " ] )
class Meta :
2019-12-05 16:57:52 +00:00
unique_together = ( " ix " , " facility " )
2020-01-08 13:29:58 -06:00
db_table = " peeringdb_ix_facility "
2018-11-08 19:45:21 +00:00
@reversion.register
class IXLan ( pdb_models . IXLanBase ) :
"""
Describes a LAN at an exchange
"""
2020-02-05 21:25:25 -06:00
# 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 )
2020-01-08 13:29:58 -06:00
ix = models . ForeignKey (
InternetExchange , on_delete = models . CASCADE , default = 0 , related_name = " ixlan_set "
)
2020-07-15 02:07:01 -05:00
# IX-F import fields
2019-04-01 15:01:36 -05:00
ixf_ixp_import_enabled = models . BooleanField ( default = False )
2020-07-15 02:07:01 -05:00
ixf_ixp_import_error = models . TextField (
_ ( " IX-F error " ) ,
blank = True ,
null = True ,
help_text = _ ( " Reason IX-F data could not be parsed " ) ,
)
ixf_ixp_import_error_notified = models . DateTimeField (
_ ( " IX-F error notification date " ) ,
help_text = _ ( " Last time we notified the exchange about the IX-F parsing issue " ) ,
null = True ,
blank = True ,
)
2020-08-03 14:51:11 -05:00
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 ,
)
2018-11-08 19:45:21 +00:00
# FIXME: delete cascade needs to be fixed in django-peeringdb, can remove
# this afterwards
class HandleRef :
tag = " ixlan "
delete_cascade = [ " ixpfx_set " , " netixlan_set " ]
class Meta :
2020-01-08 13:29:58 -06:00
db_table = " peeringdb_ixlan "
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
@classmethod
def api_cache_permissions_applicator ( cls , row , ns , user ) :
"""
Applies permissions to a row in an api - cache result
set for ixlan .
This will strip ` ixf_ixp_member_list_url ` fields for
users that dont have read permissions for them according
to ` ixf_ixp_member_list_url_visible `
Argument ( s ) :
- row ( dict ) : ixlan row from api - cache result
- ns ( str ) : ixlan namespace as determined during api - cache
result rendering
- user ( User )
"""
visible = row . get ( " ixf_ixp_member_list_url_visible " ) . lower ( )
if not user and visible == " public " :
return
namespace = f " { ns } .ixf_ixp_member_list_url. { visible } "
if not has_perms ( user , namespace , 0x01 , explicit = True ) :
try :
del row [ " ixf_ixp_member_list_url " ]
except KeyError :
pass
2019-02-15 17:46:20 +00:00
@property
def descriptive_name ( self ) :
"""
Returns a descriptive label of the ixlan for logging purposes
"""
2020-07-15 02:07:01 -05:00
return f " ixlan { self . id } { self . ix . name } "
2019-02-15 17:46:20 +00:00
2018-11-08 19:45:21 +00:00
@classmethod
def nsp_namespace_from_id ( cls , org_id , ix_id , id ) :
"""
Returns permissioning namespace for an ixlan
"""
2020-07-15 02:07:01 -05:00
# ixlan will be removed in v3 and we are already only allowing
# one ixlan per ix with matching ids so it makes sense to
# simply use the exchange's permissioning namespace here
return InternetExchange . nsp_namespace_from_id ( org_id , ix_id )
2018-11-08 19:45:21 +00:00
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this ixlan
"""
2019-12-05 16:57:52 +00:00
return self . __class__ . nsp_namespace_from_id ( self . ix . org_id , self . ix_id , self . id )
2018-11-08 19:45:21 +00:00
@property
def ixpfx_set_active ( self ) :
"""
Returns queryset of active prefixes at this ixlan
"""
return self . ixpfx_set ( manager = " handleref " ) . filter ( status = " ok " )
@property
def ixpfx_set_active_or_pending ( self ) :
"""
Returns queryset of active or pending prefixes at this ixlan
"""
2019-12-05 16:57:52 +00:00
return self . ixpfx_set ( manager = " handleref " ) . filter ( status__in = [ " ok " , " pending " ] )
2018-11-08 19:45:21 +00:00
@property
def netixlan_set_active ( self ) :
"""
Returns queryset of active netixlan objects at this ixlan
"""
2019-12-05 16:57:52 +00:00
return (
self . netixlan_set ( manager = " handleref " )
. select_related ( " network " )
. filter ( status = " ok " )
)
2018-11-08 19:45:21 +00:00
# q = NetworkIXLan.handleref.filter(ixlan_id=self.id).filter(status="ok")
# return Network.handleref.filter(id__in=[i.network_id for i in
# q]).filter(status="ok")
2020-04-10 15:00:27 +00:00
@staticmethod
def autocomplete_search_fields ( ) :
"""
Used by grappelli autocomplete to determine what
fields to search in
"""
return ( " ix__name__icontains " , )
2020-07-15 02:07:01 -05:00
def ixf_ixp_member_list_url_viewable ( self , user ) :
visible = self . ixf_ixp_member_list_url_visible . lower ( )
if not user and visible == " public " :
return True
namespace = f " { self . nsp_namespace } .ixf_ixp_member_list_url. { visible } "
return has_perms ( user , namespace , 0x01 , explicit = True )
2020-04-10 15:00:27 +00:00
def related_label ( self ) :
"""
Used by grappelli autocomplete for representation
"""
2020-07-15 02:07:01 -05:00
return f " { self . ix . name } IXLan ( { self . id } ) "
2020-04-10 15:00:27 +00:00
2018-11-08 19:45:21 +00:00
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " ix " ] )
2019-04-09 09:53:54 -05:00
def test_ipv4_address ( self , ipv4 ) :
"""
test that the ipv4 address exists in one of the prefixes in this ixlan
"""
for pfx in self . ixpfx_set_active :
if pfx . test_ip_address ( ipv4 ) :
return True
return False
def test_ipv6_address ( self , ipv6 ) :
"""
test that the ipv6 address exists in one of the prefixes in this ixlan
"""
for pfx in self . ixpfx_set_active :
if pfx . test_ip_address ( ipv6 ) :
return True
return False
2020-02-05 21:25:25 -06:00
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
2020-07-15 02:07:01 -05:00
return super ( ) . clean ( )
2020-02-05 21:25:25 -06:00
2018-11-08 19:45:21 +00:00
@reversion.create_revision ( )
def add_netixlan ( self , netixlan_info , save = True , save_others = True ) :
"""
This function allows for sane adding of netixlan object under
this ixlan .
It will take into account whether an ipaddress can be claimed from a
soft - deleted netixlan or whether or not an object already exists
that should be updated instead of creating a new netixlan instance .
Arguments :
- netixlan_info ( NetworkIXLan ) : a netixlan instance describe the netixlan
you want to add to this ixlan . Note that this instance will actually
not be saved . It only serves as an information provider .
Keyword Arguments :
- save ( bool ) : if true commit changes to db
Returns :
2020-07-15 02:07:01 -05:00
- { netixlan , created , changed , log }
2018-11-08 19:45:21 +00:00
"""
log = [ ]
2019-04-30 10:33:35 -05:00
changed = [ ]
2019-02-15 17:46:40 +00:00
created = False
2018-11-08 19:45:21 +00:00
ipv4 = netixlan_info . ipaddr4
ipv6 = netixlan_info . ipaddr6
asn = netixlan_info . asn
ipv4_valid = False
ipv6_valid = False
2019-02-15 17:46:40 +00:00
def result ( netixlan = None ) :
2019-12-05 16:57:52 +00:00
return {
" netixlan " : netixlan ,
" created " : created ,
" changed " : changed ,
" log " : log ,
}
2019-02-15 17:46:40 +00:00
2018-11-08 19:45:21 +00:00
# check if either of the provided ip addresses are a fit for ANY of
# the prefixes in this ixlan
for pfx in self . ixpfx_set_active :
if pfx . test_ip_address ( ipv4 ) :
ipv4_valid = True
if pfx . test_ip_address ( ipv6 ) :
ipv6_valid = True
# If neither ipv4 nor ipv6 match any of the prefixes, log the issue
# and bail
2020-07-15 02:07:01 -05:00
if ipv4 and not ipv4_valid :
raise ValidationError (
2020-07-26 23:36:27 -05:00
{ " ipaddr4 " : f " IPv4 { ipv4 } does not match any prefix on this ixlan " }
2020-07-15 02:07:01 -05:00
)
if ipv6 and not ipv6_valid :
raise ValidationError (
2020-07-26 23:36:27 -05:00
{ " ipaddr6 " : f " IPv6 { ipv6 } does not match any prefix on this ixlan " }
2019-12-05 16:57:52 +00:00
)
2018-11-08 19:45:21 +00:00
# Next we check if an active netixlan with the ipaddress exists in ANOTHER lan, and bail
# if it does.
2019-12-05 16:57:52 +00:00
if (
ipv4
and NetworkIXLan . objects . filter ( status = " ok " , ipaddr4 = ipv4 )
. exclude ( ixlan = self )
. count ( )
> 0
) :
2020-07-26 23:36:27 -05:00
raise ValidationError (
{ " ipaddr4 " : f " Ip address { ipv4 } already exists in another lan " }
)
2018-11-08 19:45:21 +00:00
2019-12-05 16:57:52 +00:00
if (
ipv6
and NetworkIXLan . objects . filter ( status = " ok " , ipaddr6 = ipv6 )
. exclude ( ixlan = self )
. count ( )
> 0
) :
2020-07-26 23:36:27 -05:00
raise ValidationError (
{ " ipaddr6 " : f " Ip address { ipv6 } already exists in another lan " }
)
2018-11-08 19:45:21 +00:00
# now we need to figure out if the ipaddresses already exist in this ixlan,
# we need to check ipv4 and ipv6 separately as they might exist on different
# netixlan objects.
try :
if ipv4 :
netixlan_existing_v4 = NetworkIXLan . objects . get (
2020-07-15 02:07:01 -05:00
ixlan = self , ipaddr4 = ipv4
2019-12-05 16:57:52 +00:00
)
2018-11-08 19:45:21 +00:00
else :
netixlan_existing_v4 = None
except NetworkIXLan . DoesNotExist :
netixlan_existing_v4 = None
try :
if ipv6 :
netixlan_existing_v6 = NetworkIXLan . objects . get (
2020-07-15 02:07:01 -05:00
ixlan = self , ipaddr6 = ipv6
2019-12-05 16:57:52 +00:00
)
2018-11-08 19:45:21 +00:00
else :
netixlan_existing_v6 = None
except NetworkIXLan . DoesNotExist :
netixlan_existing_v6 = None
# once we have that information we determine which netixlan object to use
if netixlan_existing_v4 and netixlan_existing_v6 :
# both ips already exist
if netixlan_existing_v4 != netixlan_existing_v6 :
# but they exist on different netixlans, so we reset the v6 netixlan
netixlan_existing_v6 . ipaddr6 = None
if save :
netixlan_existing_v6 . save ( )
# we use the existing v4 netixlan
netixlan = netixlan_existing_v4
elif netixlan_existing_v4 :
# the v4 address exsits, but v6 doesnt so we use the netixlan with the v4 match
netixlan = netixlan_existing_v4
elif netixlan_existing_v6 :
# the v6 address exists, but v4 does not so we use the netixlan with the v6 match
netixlan = netixlan_existing_v6
else :
# neither address exists, create a new netixlan object
2019-12-05 16:57:52 +00:00
netixlan = NetworkIXLan (
ixlan = self , network = netixlan_info . network , status = " ok "
)
2019-02-15 17:46:40 +00:00
created = True
2019-04-30 10:33:35 -05:00
reason = " New ip-address "
2018-11-08 19:45:21 +00:00
# now we sync the data to our determined netixlan instance
# IPv4
if ipv4 != netixlan . ipaddr4 :
2020-07-15 02:07:01 -05:00
# we need to check if this ipaddress exists on a
# soft-deleted netixlan elsewhere, and
2018-11-08 19:45:21 +00:00
# reset if so.
2020-07-15 02:07:01 -05:00
2019-12-05 16:57:52 +00:00
for other in NetworkIXLan . objects . filter (
ipaddr4 = ipv4 , status = " deleted "
) . exclude ( asn = asn ) :
2020-07-15 02:07:01 -05:00
other . ipaddr4 = None
other . notes = f " Ip address { ipv4 } was claimed by other netixlan "
if save or save_others :
other . save ( )
2018-11-08 19:45:21 +00:00
netixlan . ipaddr4 = ipv4
2019-04-30 10:33:35 -05:00
changed . append ( " ipaddr4 " )
2018-11-08 19:45:21 +00:00
# IPv6
if ipv6 != netixlan . ipaddr6 :
2020-07-15 02:07:01 -05:00
# we need to check if this ipaddress exists on a
# soft-deleted netixlan elsewhere, and
2018-11-08 19:45:21 +00:00
# reset if so.
2020-07-15 02:07:01 -05:00
2019-12-05 16:57:52 +00:00
for other in NetworkIXLan . objects . filter (
ipaddr6 = ipv6 , status = " deleted "
) . exclude ( asn = asn ) :
2020-07-15 02:07:01 -05:00
other . ipaddr6 = None
other . notes = f " Ip address { ipv6 } was claimed by other netixlan "
if save or save_others :
other . save ( )
2018-11-08 19:45:21 +00:00
netixlan . ipaddr6 = ipv6
2019-04-30 10:33:35 -05:00
changed . append ( " ipaddr6 " )
2018-11-08 19:45:21 +00:00
# Is the netixlan a routeserver ?
if netixlan_info . is_rs_peer != netixlan . is_rs_peer :
netixlan . is_rs_peer = netixlan_info . is_rs_peer
2019-04-30 10:33:35 -05:00
changed . append ( " is_rs_peer " )
2018-11-08 19:45:21 +00:00
2020-07-26 23:36:27 -05:00
# Is the netixlan operational?
if netixlan_info . operational != netixlan . operational :
netixlan . operational = netixlan_info . operational
changed . append ( " operational " )
2018-11-08 19:45:21 +00:00
# Speed
2019-12-05 16:57:52 +00:00
if netixlan_info . speed != netixlan . speed and (
netixlan_info . speed > 0 or netixlan . speed is None
) :
2018-11-08 19:45:21 +00:00
netixlan . speed = netixlan_info . speed
2019-04-30 10:33:35 -05:00
changed . append ( " speed " )
2018-11-08 19:45:21 +00:00
# ASN
if netixlan_info . asn != netixlan . asn :
netixlan . asn = netixlan_info . asn
2019-04-30 10:33:35 -05:00
changed . append ( " asn " )
2018-11-08 19:45:21 +00:00
# Network
if netixlan_info . network . id != netixlan . network . id :
netixlan . network = netixlan_info . network
2019-04-30 10:33:35 -05:00
changed . append ( " network_id " )
2018-11-08 19:45:21 +00:00
2020-07-26 23:36:27 -05:00
if save and ( changed or netixlan . status == " deleted " ) :
2018-11-08 19:45:21 +00:00
netixlan . status = " ok "
2020-07-26 23:36:27 -05:00
netixlan . full_clean ( )
2018-11-08 19:45:21 +00:00
netixlan . save ( )
2019-02-15 17:46:40 +00:00
return result ( netixlan )
2018-11-08 19:45:21 +00:00
class IXLanIXFMemberImportAttempt ( models . Model ) :
"""
Holds information about the most recent ixf member import
attempt for an ixlan
"""
2019-12-05 16:57:52 +00:00
ixlan = models . OneToOneField (
IXLan ,
on_delete = models . CASCADE ,
primary_key = True ,
related_name = " ixf_import_attempt " ,
)
2018-11-08 19:45:21 +00:00
updated = models . DateTimeField ( auto_now = True )
info = models . TextField ( null = True , blank = True )
class IXLanIXFMemberImportLog ( models . Model ) :
"""
Import log of a IXF member import that changed or added at least one
netixlan under the specified ixlans
"""
2019-12-05 16:57:52 +00:00
2020-01-08 13:29:58 -06:00
ixlan = models . ForeignKey (
IXLan , on_delete = models . CASCADE , related_name = " ixf_import_log_set "
)
2018-11-08 19:45:21 +00:00
created = models . DateTimeField ( auto_now_add = True )
updated = models . DateTimeField ( auto_now = True )
2020-07-15 02:07:01 -05:00
class Meta :
2018-11-08 19:45:21 +00:00
verbose_name = _ ( " IXF Import Log " )
verbose_name_plural = _ ( " IXF Import Logs " )
@reversion.create_revision ( )
def rollback ( self ) :
"""
Attempt to rollback the changes described in this log
"""
2020-07-15 02:07:01 -05:00
for entry in self . entries . all ( ) . order_by ( " -id " ) :
2018-11-08 19:45:21 +00:00
if entry . rollback_status ( ) == 0 :
if entry . version_before :
entry . version_before . revert ( )
2020-07-15 02:07:01 -05:00
related = self . entries . filter ( netixlan = entry . netixlan , ) . exclude (
id = entry . id
)
for _entry in related . order_by ( " -id " ) :
try :
_entry . version_before . revert ( )
except :
break
2018-11-08 19:45:21 +00:00
elif entry . netixlan . status == " ok " :
2020-07-15 02:07:01 -05:00
entry . netixlan . ipaddr4 = None
entry . netixlan . ipaddr6 = None
2018-11-08 19:45:21 +00:00
entry . netixlan . delete ( )
class IXLanIXFMemberImportLogEntry ( models . Model ) :
"""
IXF member import log entry that holds the affected netixlan and
the netixlan ' s version after the change, which can be used to rollback
the change
"""
2019-12-05 16:57:52 +00:00
2020-01-08 13:29:58 -06:00
log = models . ForeignKey (
IXLanIXFMemberImportLog , on_delete = models . CASCADE , related_name = " entries "
)
2019-12-05 16:57:52 +00:00
netixlan = models . ForeignKey (
2020-01-08 13:29:58 -06:00
" peeringdb_server.NetworkIXLan " ,
on_delete = models . CASCADE ,
related_name = " ixf_import_log_entries " ,
2019-12-05 16:57:52 +00:00
)
version_before = models . ForeignKey (
2020-01-08 13:29:58 -06:00
reversion . models . Version ,
on_delete = models . CASCADE ,
null = True ,
related_name = " ixf_import_log_before " ,
2019-12-05 16:57:52 +00:00
)
version_after = models . ForeignKey (
2020-01-08 13:29:58 -06:00
reversion . models . Version ,
on_delete = models . CASCADE ,
related_name = " ixf_import_log_after " ,
2019-12-05 16:57:52 +00:00
)
2019-09-07 01:08:37 -05:00
action = models . CharField ( max_length = 255 , null = True , blank = True )
reason = models . CharField ( max_length = 255 , null = True , blank = True )
2020-07-15 02:07:01 -05:00
class Meta :
2018-11-08 19:45:21 +00:00
verbose_name = _ ( " IXF Import Log Entry " )
verbose_name_plural = _ ( " IXF Import Log Entries " )
2019-09-07 01:08:37 -05:00
@property
def changes ( self ) :
"""
Returns a dict of changes between the netixlan version
saved by the ix - f import and the version before
Fields ` created ` , ` updated ` and ` version ` will be ignored
"""
if not self . version_before :
return { }
data_before = self . version_before . field_dict
2019-12-05 16:57:52 +00:00
data_after = self . version_after . field_dict
2019-09-07 01:08:37 -05:00
rv = { }
2020-01-08 13:29:58 -06:00
for k , v in list ( data_after . items ( ) ) :
2019-09-07 01:08:37 -05:00
if k in [ " created " , " updated " , " version " ] :
continue
v2 = data_before . get ( k )
if v != v2 :
if isinstance ( v , ipaddress . IPv4Address ) or isinstance (
2019-12-05 16:57:52 +00:00
v , ipaddress . IPv6Address
) :
2019-09-07 01:08:37 -05:00
rv [ k ] = str ( v )
else :
rv [ k ] = v
return rv
2018-11-08 19:45:21 +00:00
def rollback_status ( self ) :
recent_version = reversion . models . Version . objects . get_for_object (
2019-12-05 16:57:52 +00:00
self . netixlan
) . first ( )
2018-11-08 19:45:21 +00:00
if self . version_after == recent_version :
if self . netixlan . status == " deleted " :
conflict_v4 , conflict_v6 = self . netixlan . ipaddress_conflict ( )
if conflict_v4 or conflict_v6 :
return 2
return 0
elif self . version_before == recent_version :
return - 1
return 1
2020-07-26 23:36:27 -05:00
class NetworkProtocolsDisabled ( ValueError ) :
"""
raised when a network has both ipv6 and ipv4 support
disabled during ix - f import
"""
2020-07-15 02:07:01 -05:00
class IXFMemberData ( pdb_models . NetworkIXLanBase ) :
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
Describes a potential data update that arose during an ix - f import
attempt for a specific member ( asn , ip4 , ip6 ) to netixlan
( asn , ip4 , ip6 ) where the importer could not complete the
update automatically .
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
data = models . TextField (
null = False ,
default = " {} " ,
help_text = _ ( " JSON snapshot of the ix-f member data that " " created this entry " ) ,
2020-01-08 13:29:58 -06:00
)
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
log = models . TextField ( blank = True , help_text = _ ( " Activity for this entry " ) )
2019-02-15 17:46:20 +00:00
2020-07-15 02:07:01 -05:00
dismissed = models . BooleanField (
default = False ,
help_text = _ (
" Network ' s dismissal of this proposed change, which will hide it until "
" from the customer facing network view "
) ,
)
2018-11-08 19:45:21 +00:00
2020-08-03 14:51:11 -05:00
is_rs_peer = models . BooleanField (
default = None , null = True , blank = True , help_text = _ ( " RS Peer " )
)
2020-07-15 02:07:01 -05:00
error = models . TextField (
null = True ,
blank = True ,
help_text = _ ( " Trying to apply data to peeringdb raised an issue " ) ,
)
reason = models . CharField ( max_length = 255 , default = " " )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
fetched = models . DateTimeField ( _ ( " Last Fetched " ) , )
ixlan = models . ForeignKey ( IXLan , related_name = " ixf_set " , on_delete = models . CASCADE )
2020-07-26 23:36:27 -05:00
requirement_of = models . ForeignKey (
" self " ,
on_delete = models . CASCADE ,
related_name = " requirement_set " ,
null = True ,
blank = True ,
help_text = _ (
" Requirement of another IXFMemberData entry "
" and will be applied alongside it "
) ,
)
deskpro_ref = models . CharField (
max_length = 32 ,
null = True ,
blank = True ,
help_text = _ ( " Ticket reference on the DeskPRO side " ) ,
)
deskpro_id = models . IntegerField (
null = True , blank = True , help_text = _ ( " Ticket id on the DeskPRO side " )
)
2020-07-15 02:07:01 -05:00
# field names of fields that can receive
# modifications from ix-f
data_fields = [
" speed " ,
" operational " ,
" is_rs_peer " ,
]
class Meta :
db_table = " peeringdb_ixf_member_data "
verbose_name = _ ( " IXF Member Data " )
verbose_name_plural = _ ( " IXF Member Data " )
class HandleRef :
tag = " ixfmember "
2018-11-08 19:45:21 +00:00
2020-02-05 21:26:21 -06:00
@classmethod
2020-07-26 23:36:27 -05:00
def id_filters ( cls , asn , ipaddr4 , ipaddr6 , check_protocols = True ) :
2020-02-05 21:26:21 -06:00
"""
2020-07-15 02:07:01 -05:00
returns a dict of filters to use with a
IXFMemberData or NetworkIXLan query set
to retrieve a unique entry
2020-02-05 21:26:21 -06:00
"""
2020-07-26 23:36:27 -05:00
net = Network . objects . get ( asn = asn )
ipv4_support = net . ipv4_support or not check_protocols
ipv6_support = net . ipv6_support or not check_protocols
2020-07-15 02:07:01 -05:00
filters = { " asn " : asn }
2020-02-05 21:26:21 -06:00
2020-07-26 23:36:27 -05:00
if ipv4_support :
if ipaddr4 :
filters [ " ipaddr4 " ] = ipaddr4
else :
filters [ " ipaddr4__isnull " ] = True
2020-02-05 21:26:21 -06:00
2020-07-26 23:36:27 -05:00
if ipv6_support :
if ipaddr6 :
filters [ " ipaddr6 " ] = ipaddr6
else :
filters [ " ipaddr6__isnull " ] = True
2020-02-05 21:26:21 -06:00
2020-07-15 02:07:01 -05:00
return filters
2020-02-05 21:26:21 -06:00
2020-07-15 02:07:01 -05:00
@classmethod
def instantiate ( cls , asn , ipaddr4 , ipaddr6 , ixlan , * * kwargs ) :
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
Returns an IXFMemberData object .
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
It will take into consideration whether or not an instance
for this object already exists ( as identified by asn and ip
addresses )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
It will also update the value of ` fetched ` to now
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
Keyword Argument ( s ) :
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
- speed ( int = 0 ) : network speed ( mbit )
- operational ( bool = True ) : peer is operational
- is_rs_peer ( bool = False ) : peer is route server
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
fetched = datetime . datetime . now ( ) . replace ( tzinfo = UTC ( ) )
2020-07-26 23:36:27 -05:00
net = Network . objects . get ( asn = asn )
validate_network_protocols = kwargs . get ( " validate_network_protocols " , True )
for_deletion = kwargs . get ( " delete " , False )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
try :
2020-07-26 23:36:27 -05:00
id_filters = cls . id_filters ( asn , ipaddr4 , ipaddr6 )
instances = cls . objects . filter ( * * id_filters )
if not instances . exists ( ) :
raise cls . DoesNotExist ( )
if instances . count ( ) > 1 :
# this only happens when a network switches on/off
# ipv4/ipv6 protocol support inbetween importer
# runs.
for instance in instances :
if ipaddr4 != instance . ipaddr4 or ipaddr6 != instance . ipaddr6 :
instance . delete ( hard = True )
instance = cls . objects . get ( * * id_filters )
else :
instance = instances . first ( )
2020-07-15 02:07:01 -05:00
for field in cls . data_fields :
setattr ( instance , f " previous_ { field } " , getattr ( instance , field ) )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
instance . _previous_data = instance . data
instance . _previous_error = instance . error
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
instance . fetched = fetched
instance . _meta . get_field ( " updated " ) . auto_now = False
instance . save ( )
instance . _meta . get_field ( " updated " ) . auto_now = True
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
except cls . DoesNotExist :
2020-07-26 23:36:27 -05:00
ip_args = { }
if net . ipv4_support or not ipaddr4 or for_deletion :
ip_args . update ( ipaddr4 = ipaddr4 )
if net . ipv6_support or not ipaddr6 or for_deletion :
ip_args . update ( ipaddr6 = ipaddr6 )
if not ip_args and validate_network_protocols :
raise NetworkProtocolsDisabled (
_ (
" No suitable ipaddresses when validating against the enabled network protocols "
)
)
instance = cls ( asn = asn , status = " ok " , * * ip_args )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
instance . speed = kwargs . get ( " speed " , 0 )
instance . operational = kwargs . get ( " operational " , True )
2020-08-03 14:51:11 -05:00
instance . is_rs_peer = kwargs . get ( " is_rs_peer " )
2020-07-15 02:07:01 -05:00
instance . ixlan = ixlan
instance . fetched = fetched
2020-07-26 23:36:27 -05:00
instance . for_deletion = for_deletion
if ipaddr4 :
instance . init_ipaddr4 = ipaddress . ip_address ( ipaddr4 )
else :
instance . init_ipaddr4 = None
if ipaddr6 :
instance . init_ipaddr6 = ipaddress . ip_address ( ipaddr6 )
else :
instance . init_ipaddr6 = None
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
if " data " in kwargs :
instance . set_data ( kwargs . get ( " data " ) )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
return instance
2018-11-08 19:45:21 +00:00
@classmethod
2020-07-15 02:07:01 -05:00
def get_for_network ( cls , net ) :
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
Returns aueryset for IXFMemberData objects that match
a network ' s asn
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
Argument ( s ) :
- net ( Network )
"""
return cls . objects . filter ( asn = net . asn )
2018-11-08 19:45:21 +00:00
@classmethod
2020-07-15 02:07:01 -05:00
def dismissed_for_network ( cls , net ) :
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
Returns queryset for IXFMemberData objects that match
a network ' s asn and are currenlty flagged as dismissed
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
Argument ( s ) :
- net ( Network )
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
qset = cls . get_for_network ( net ) . select_related ( " ixlan " , " ixlan__ix " )
qset = qset . filter ( dismissed = True )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
dismissed = { }
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
for ixf_member_data in qset :
ix = ixf_member_data . ix
if ix . id not in dismissed :
dismissed [ ix . id ] = ix
return dismissed
2018-11-08 19:45:21 +00:00
@classmethod
2020-07-15 02:07:01 -05:00
def proposals_for_network ( cls , net ) :
"""
Returns a dict containing actionable proposals for
a network
` ` `
{
< ix_id > : {
" ix " : InternetExchange ,
" add " : list ( IXFMemberData ) ,
" modify " : list ( IXFMemberData ) ,
" delete " : list ( IXFMemberData ) ,
}
}
` ` `
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
Argument ( s ) :
- net ( Network )
2018-11-08 19:45:21 +00:00
"""
2020-07-15 02:07:01 -05:00
qset = cls . get_for_network ( net ) . select_related ( " ixlan " , " ixlan__ix " )
2018-11-08 19:45:21 +00:00
2020-07-15 02:07:01 -05:00
proposals = { }
for ixf_member_data in qset :
action = ixf_member_data . action
error = ixf_member_data . error
2020-07-26 23:36:27 -05:00
# not actionable for anyone
2020-07-15 02:07:01 -05:00
if action == " noop " :
continue
2020-07-26 23:36:27 -05:00
# not actionable for network
2020-07-15 02:07:01 -05:00
if not ixf_member_data . actionable_for_network :
continue
2020-07-26 23:36:27 -05:00
# dismissed by network
2020-07-15 02:07:01 -05:00
if ixf_member_data . dismissed :
continue
ix_id = ixf_member_data . ix . id
if ix_id not in proposals :
proposals [ ix_id ] = {
" ix " : ixf_member_data . ix ,
" add " : [ ] ,
" delete " : [ ] ,
" modify " : [ ] ,
}
proposals [ ix_id ] [ action ] . append ( ixf_member_data )
return proposals
@property
def previous_data ( self ) :
return getattr ( self , " _previous_data " , " {} " )
@property
def previous_error ( self ) :
return getattr ( self , " _previous_error " , None )
@property
def json ( self ) :
"""
Returns dict for self . data
"""
return json . loads ( self . data )
@property
def net ( self ) :
"""
Returns the Network instance related to
this entry
"""
if not hasattr ( self , " _net " ) :
self . _net = Network . objects . get ( asn = self . asn )
return self . _net
@property
def actionable_for_network ( self ) :
"""
Returns whether or not the proposed action by
this IXFMemberData instance is actionable by
the network
"""
error = self . error
if error and " address outside of prefix " in error :
return False
2020-07-26 23:36:27 -05:00
if error and " does not match any prefix " in error :
return False
if error and " speed value " in error :
return False
2020-07-15 02:07:01 -05:00
return True
2020-07-26 23:36:27 -05:00
@property
def actionable_error ( self ) :
"""
Returns whether or not the error is actionable
by exchange or network .
If actionable will return self . error otherwise
will return None
"""
if not self . error :
return None
try :
error_data = json . loads ( self . error )
except :
return None
IPADDR_EXIST = " already exists "
DELETED_NETIXLAN_BAD_ASN = " This entity was created for the ASN "
if IPADDR_EXIST in error_data . get ( " ipaddr4 " , [ " " ] ) [ 0 ] :
for requirement in self . requirements :
if requirement . netixlan . ipaddr4 == self . ipaddr4 :
return None
if NetworkIXLan . objects . filter (
ipaddr4 = self . ipaddr4 , status = " deleted "
) . exists ( ) :
return None
if IPADDR_EXIST in error_data . get ( " ipaddr6 " , [ " " ] ) [ 0 ] :
for requirement in self . requirements :
if requirement . netixlan . ipaddr6 == self . ipaddr6 :
return None
if NetworkIXLan . objects . filter (
ipaddr6 = self . ipaddr6 , status = " deleted "
) . exists ( ) :
return None
if DELETED_NETIXLAN_BAD_ASN in error_data . get ( " __all__ " , [ " " ] ) [ 0 ] :
if self . netixlan . status == " deleted " :
return None
return self . error
2020-07-15 02:07:01 -05:00
@property
def net_contacts ( self ) :
"""
Returns a list of email addresses that
are suitable contact points for conflict resolution
at the network ' s end
"""
qset = self . net . poc_set_active . exclude ( email = " " )
qset = qset . exclude ( email__isnull = True )
2020-07-26 23:36:27 -05:00
role_priority = [ " Technical " , " NOC " , " Policy " ]
2020-07-15 02:07:01 -05:00
contacts = [ ]
for role in role_priority :
for poc in qset . filter ( role = role ) :
contacts . append ( poc . email )
if contacts :
break
return list ( set ( contacts ) )
@property
def ix_contacts ( self ) :
"""
Returns a list of email addresses that
are suitable contact points for conflict resolution
at the exchange end
"""
return [ self . ix . tech_email or self . ix . policy_email ]
@property
def ix ( self ) :
"""
Returns the InternetExchange instance related to
this entry
"""
if not hasattr ( self , " _ix " ) :
self . _ix = self . ixlan . ix
return self . _ix
@property
def ixf_id ( self ) :
"""
Returns a tuple that identifies the ix - f member
as a unqiue record by asn , ip4 and ip6 address
"""
return ( self . asn , self . ipaddr4 , self . ipaddr6 )
@property
def ixf_id_pretty_str ( self ) :
ipaddr4 = self . ipaddr4 or _ ( " IPv4 not set " )
ipaddr6 = self . ipaddr6 or _ ( " IPv6 not set " )
return f " AS { self . asn } - { ipaddr4 } - { ipaddr6 } "
2020-07-26 23:36:27 -05:00
@property
def actionable_changes ( self ) :
requirements = self . requirements
_changes = self . changes
for requirement in self . requirements :
_changes . update ( self . _changes ( requirement . netixlan ) )
if self . ipaddr4_on_requirement :
_changes . update ( ipaddr4 = self . ipaddr4_on_requirement )
if self . ipaddr6_on_requirement :
_changes . update ( ipaddr6 = self . ipaddr6_on_requirement )
return _changes
2020-07-15 02:07:01 -05:00
@property
def changes ( self ) :
"""
Returns a dict of changes ( field , value )
between this entry and the related netixlan
If an empty dict is returned that means no changes
` ` `
{
< field_name > : {
" from " : < value > ,
" to : <value>
}
}
` ` `
"""
netixlan = self . netixlan
2020-07-26 23:36:27 -05:00
return self . _changes ( netixlan )
def _changes ( self , netixlan ) :
2020-07-15 02:07:01 -05:00
changes = { }
2020-07-26 23:36:27 -05:00
if self . marked_for_removal or not netixlan :
2020-07-15 02:07:01 -05:00
return changes
2020-08-03 14:51:11 -05:00
if (
self . modify_is_rs_peer
and self . is_rs_peer is not None
and netixlan . is_rs_peer != self . is_rs_peer
) :
2020-07-15 02:07:01 -05:00
changes . update (
is_rs_peer = { " from " : netixlan . is_rs_peer , " to " : self . is_rs_peer }
)
2020-08-03 14:51:11 -05:00
if self . modify_speed and self . speed > 0 and netixlan . speed != self . speed :
2020-07-15 02:07:01 -05:00
changes . update ( speed = { " from " : netixlan . speed , " to " : self . speed } )
if netixlan . operational != self . operational :
changes . update (
operational = { " from " : netixlan . operational , " to " : self . operational }
)
if netixlan . status != self . status :
changes . update ( status = { " from " : netixlan . status , " to " : self . status } )
return changes
2020-08-03 14:51:11 -05:00
@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
2020-07-15 02:07:01 -05:00
@property
def changed_fields ( self ) :
"""
Returns a comma separated string of field names
for changes proposed by this IXFMemberData instance
"""
return " , " . join ( list ( self . changes . keys ( ) ) )
@property
def remote_changes ( self ) :
"""
Returns a dict of changed fields between previously
fetched IX - F data and current IX - F data
If an empty dict is returned that means no changes
` ` `
{
< field_name > : {
" from " : < value > ,
" to : <value>
}
}
` ` `
"""
if not self . id and self . netixlan . id :
return { }
changes = { }
for field in self . data_fields :
old_v = getattr ( self , f " previous_ { field } " , None )
v = getattr ( self , field )
if old_v is not None and v != old_v :
changes [ field ] = { " from " : old_v , " to " : v }
return changes
@property
def remote_data_missing ( self ) :
"""
Returns whether or not this IXFMemberData entry
had data at the IX - F source .
If not it indicates that it does not exist at the
ix - f source
"""
return self . data == " {} " or not self . data
@property
def marked_for_removal ( self ) :
"""
Returns whether or not this entry implies that
the related netixlan should be removed .
We do this by checking if the ix - f data was provided
or not
"""
if not self . netixlan . id or self . netixlan . status == " deleted " :
# edge-case that should not really happen
# non-existing netixlan cannot be removed
return False
return self . remote_data_missing
@property
def net_present_at_ix ( self ) :
"""
Returns whether or not the network associated with
this IXFMemberData instance currently has a presence
at the exchange associated with this IXFMemberData
instance
"""
return NetworkIXLan . objects . filter (
ixlan = self . ixlan , network = self . net , status = " ok "
) . exists ( )
@property
def action ( self ) :
"""
Returns the implied action of applying this
entry to peeringdb
Will return either " add " , " modify " , " delete " or " noop "
"""
has_data = self . remote_data_missing == False
2020-07-26 23:36:27 -05:00
action = " noop "
2020-07-15 02:07:01 -05:00
if has_data :
if not self . netixlan . id :
2020-07-26 23:36:27 -05:00
action = " add "
2020-07-15 02:07:01 -05:00
2020-07-26 23:36:27 -05:00
elif self . status == " ok " and self . netixlan . status == " deleted " :
action = " add "
2020-07-15 02:07:01 -05:00
2020-07-26 23:36:27 -05:00
elif self . changes :
action = " modify "
2020-07-15 02:07:01 -05:00
else :
if self . marked_for_removal :
2020-07-26 23:36:27 -05:00
action = " delete "
# the proposal is to add a netixlan, but we have
# the requirement of a deletion of another netixlan
# that has one of the ips set but not the other.
#
# action re-classified to modify (#770)
if action == " add " and self . has_requirements :
action = " modify "
return action
@property
def has_requirements ( self ) :
"""
Return whether or not this IXFMemberData has
other IXFMemberData objects as requirements
"""
return len ( self . requirements ) > 0
@property
def requirements ( self ) :
"""
Returns list of all IXFMemberData objects
that are still active requirements for this
IXFMemberData object
"""
return [
requirement
for requirement in self . requirement_set . all ( )
# if requirement.action != "noop"
]
@property
def primary_requirement ( self ) :
"""
Return the initial requirement IXFMemberData
for this IXFMemberData instance , None if there
isn ' t any
"""
try :
return self . requirements [ 0 ]
except IndexError :
return None
2020-07-15 02:07:01 -05:00
2020-07-26 23:36:27 -05:00
@property
def secondary_requirements ( self ) :
"""
Return a list of secondary requirement IXFMemberData
objects for this IXFMemberData object . Currently this
only happens on add proposals that require two netixlans
to be deleted because both ipaddresses exist on separate
netixlans ( #770)
"""
return self . requirements [ 1 : ]
@property
def ipaddr4_on_requirement ( self ) :
"""
Returns true if the ipv4 address claimed by this IXFMemberData
object exists on one of it ' s requirement IXFMemberData objects
"""
ipaddr4 = self . ipaddr4
if not ipaddr4 and hasattr ( self , " init_ipaddr4 " ) :
ipaddr4 = self . init_ipaddr4
if not ipaddr4 :
return False
for requirement in self . requirements :
if requirement . ipaddr4 == ipaddr4 :
return True
return False
@property
def ipaddr6_on_requirement ( self ) :
"""
Returns true if the ipv6 address claimed by this IXFMemberData
object exists on one of it ' s requirement IXFMemberData objects
"""
ipaddr6 = self . ipaddr6
if not ipaddr6 and hasattr ( self , " init_ipaddr6 " ) :
ipaddr6 = self . init_ipaddr6
if not ipaddr6 :
return False
for requirement in self . requirements :
if requirement . ipaddr6 == ipaddr6 :
return True
return False
2020-07-15 02:07:01 -05:00
@property
def netixlan ( self ) :
"""
Will either return a matching existing netixlan
instance ( asn , ip4 , ip6 ) or a new netixlan if
a matching netixlan does not currently exist .
Any new netixlan will NOT be saved at this point .
Note that the netixlan that matched may be currently
soft - deleted ( status == " deleted " )
"""
if not hasattr ( self , " _netixlan " ) :
2020-07-26 23:36:27 -05:00
if not hasattr ( self , " for_deletion " ) :
self . for_deletion = self . remote_data_missing
2020-07-15 02:07:01 -05:00
2020-07-26 23:36:27 -05:00
try :
if self . for_deletion :
filters = self . id_filters (
self . asn , self . ipaddr4 , self . ipaddr6 , check_protocols = False
)
2020-07-15 02:07:01 -05:00
else :
2020-07-26 23:36:27 -05:00
filters = self . id_filters ( self . asn , self . ipaddr4 , self . ipaddr6 )
2020-07-15 02:07:01 -05:00
2020-07-26 23:36:27 -05:00
if " ipaddr6 " not in filters and " ipaddr4 " not in filters :
raise NetworkIXLan . DoesNotExist ( )
2020-07-15 02:07:01 -05:00
self . _netixlan = NetworkIXLan . objects . get ( * * filters )
except NetworkIXLan . DoesNotExist :
2020-08-03 14:51:11 -05:00
is_rs_peer = self . is_rs_peer
if is_rs_peer is None :
is_rs_peer = False
2020-07-15 02:07:01 -05:00
self . _netixlan = NetworkIXLan (
ipaddr4 = self . ipaddr4 ,
ipaddr6 = self . ipaddr6 ,
speed = self . speed ,
asn = self . asn ,
operational = self . operational ,
2020-08-03 14:51:11 -05:00
is_rs_peer = is_rs_peer ,
2020-07-15 02:07:01 -05:00
ixlan = self . ixlan ,
network = self . net ,
status = " ok " ,
)
return self . _netixlan
@property
def netixlan_exists ( self ) :
"""
Returns whether or not an active netixlan exists
for this IXFMemberData instance .
"""
return self . netixlan . id and self . netixlan . status != " deleted "
def __str__ ( self ) :
parts = [
self . ixlan . ix . name ,
f " AS { self . asn } " ,
]
if self . ipaddr4 :
parts . append ( f " { self . ipaddr4 } " )
else :
parts . append ( " No IPv4 " )
if self . ipaddr6 :
parts . append ( f " { self . ipaddr6 } " )
else :
parts . append ( " No IPv6 " )
return " " . join ( parts )
2020-07-26 23:36:27 -05:00
def set_requirement ( self , ixf_member_data , save = True ) :
"""
Sets another IXFMemberData object to be a requirement
of the resolution of this IXFMemberData object
"""
if not ixf_member_data :
return
if ixf_member_data in self . requirements :
return
if ixf_member_data . netixlan == self . netixlan :
return
ixf_member_data . requirement_of = self
if save :
ixf_member_data . save ( )
return ixf_member_data
def apply_requirements ( self , save = True ) :
"""
Apply all requirements
"""
for requirement in self . requirements :
requirement . apply ( save = save )
2020-07-15 02:07:01 -05:00
def apply ( self , user = None , comment = None , save = True ) :
"""
Applies the data .
This will either create , update or delete a netixlan
object
Will return a dict containing action and netixlan
affected
` ` `
{
" action " : < action ( str ) >
" netixlan " : < NetworkIXLan >
}
` ` `
Keyword Argument ( s ) :
- user ( User ) : if set will set the user on the
reversion revision
- comment ( str ) : if set will set the comment on the
reversion revision
- save ( bool = True ) : only persist changes to the database
if this is True
"""
if user :
reversion . set_user ( user )
if comment :
reversion . set_comment ( comment )
2020-07-26 23:36:27 -05:00
self . apply_requirements ( save = save )
2020-07-15 02:07:01 -05:00
action = self . action
netixlan = self . netixlan
changes = self . changes
if action == " add " :
self . validate_speed ( )
2020-07-26 23:36:27 -05:00
if not self . net . ipv6_support :
netixlan . ipaddr6 = None
if not self . net . ipv4_support :
netixlan . ipaddr4 = None
2020-07-15 02:07:01 -05:00
result = self . ixlan . add_netixlan ( netixlan , save = save , save_others = save )
2020-07-26 23:36:27 -05:00
2020-07-15 02:07:01 -05:00
self . _netixlan = netixlan = result [ " netixlan " ]
elif action == " modify " :
self . validate_speed ( )
2020-08-03 14:51:11 -05:00
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
2020-07-15 02:07:01 -05:00
netixlan . operational = self . operational
if save :
2020-07-26 23:36:27 -05:00
netixlan . full_clean ( )
2020-07-15 02:07:01 -05:00
netixlan . save ( )
elif action == " delete " :
2020-07-26 23:36:27 -05:00
2020-07-15 02:07:01 -05:00
if save :
netixlan . delete ( )
2020-07-26 23:36:27 -05:00
return { " action " : action , " netixlan " : netixlan , " ixf_member_data " : self }
2020-07-15 02:07:01 -05:00
def validate_speed ( self ) :
"""
Speed errors in ix - f data are raised during parse
and speed will be on the attribute
In order to properly handle invalid speed values
we check if speed is 0 and if there was a parsing
error for it , and if so raise a validation error
TODO : find a better way to do this
"""
if self . speed == 0 and self . error :
2020-07-26 23:36:27 -05:00
error_data = json . loads ( self . error )
if " speed " in self . error :
raise ValidationError ( error_data )
2020-07-15 02:07:01 -05:00
def save_without_update ( self ) :
self . _meta . get_field ( " updated " ) . auto_now = False
self . save ( )
self . _meta . get_field ( " updated " ) . auto_now = True
def grab_validation_errors ( self ) :
"""
This will attempt to validate the netixlan associated
with this IXFMemberData instance .
Any validation errors will be stored to self . error
"""
try :
self . netixlan . full_clean ( )
except ValidationError as exc :
2020-07-26 23:36:27 -05:00
self . error = json . dumps ( exc , cls = ValidationErrorEncoder )
2020-07-15 02:07:01 -05:00
def set_resolved ( self , save = True ) :
"""
Marks this IXFMemberData instance as resolved and
send out notifications to ac , ix and net if
warranted
this will delete the IXFMemberData instance
"""
2020-07-26 23:36:27 -05:00
if self . id and save and not self . requirement_of :
2020-07-15 02:07:01 -05:00
self . delete ( hard = True )
2020-07-26 23:36:27 -05:00
return True
2020-07-15 02:07:01 -05:00
def set_conflict ( self , error = None , save = True ) :
"""
Persist this IXFMemberData instance and send out notifications
for conflict ( validation issues ) for modifications proposed
to the corresponding netixlan to ac , ix and net as warranted
as warranted
"""
2020-07-26 23:36:27 -05:00
if not self . id :
existing_conflict = IXFMemberData . objects . filter (
asn = self . asn , error__isnull = False
)
if self . ipaddr4 and self . ipaddr6 :
existing_conflict = existing_conflict . filter (
models . Q ( ipaddr4 = self . ipaddr4 ) | models . Q ( ipaddr6 = self . ipaddr6 )
)
elif self . ipaddr4 :
existing_conflict = existing_conflict . filter ( ipaddr4 = self . ipaddr4 )
elif self . ipaddr6 :
existing_conflict = existing_conflict . filter ( ipaddr6 = self . ipaddr6 )
if existing_conflict . exists ( ) :
return None
2020-07-15 02:07:01 -05:00
if ( self . remote_changes or ( error and not self . previous_error ) ) and save :
2020-07-26 23:36:27 -05:00
if error :
self . error = json . dumps ( error , cls = ValidationErrorEncoder )
else :
self . error = None
2020-07-15 02:07:01 -05:00
self . dismissed = False
self . save ( )
2020-07-26 23:36:27 -05:00
return True
2020-07-15 02:07:01 -05:00
elif self . previous_data != self . data and save :
# since remote_changes only tracks changes to the
# relevant data fields speed, operational and is_rs_peer
# we check if the remote data has changed in general
# and force a save if it did
self . save_without_update ( )
def set_update ( self , save = True , reason = " " ) :
"""
Persist this IXFMemberData instance and send out notifications
for proposed modification to the corresponding netixlan
instance to ac , ix and net as warranted
"""
self . reason = reason
if ( ( self . changes and not self . id ) or self . remote_changes ) and save :
self . grab_validation_errors ( )
self . dismissed = False
self . save ( )
2020-07-26 23:36:27 -05:00
return True
2020-07-15 02:07:01 -05:00
elif self . previous_data != self . data and save :
# since remote_changes only tracks changes to the
# relevant data fields speed, operational and is_rs_peer
# we check if the remote data has changed in general
# and force a save if it did
self . save_without_update ( )
def set_add ( self , save = True , reason = " " ) :
"""
Persist this IXFMemberData instance and send out notifications
for proposed creation of netixlan instance to ac , ix and net
as warranted
"""
self . reason = reason
2020-07-26 23:36:27 -05:00
2020-07-15 02:07:01 -05:00
if not self . id and save :
self . grab_validation_errors ( )
2020-08-03 14:51:11 -05:00
2020-07-15 02:07:01 -05:00
self . save ( )
2020-07-26 23:36:27 -05:00
return True
2020-07-15 02:07:01 -05:00
elif self . previous_data != self . data and save :
# since remote_changes only tracks changes to the
# relevant data fields speed, operational and is_rs_peer
# we check if the remote data has changed in general
# and force a save if it did
self . save_without_update ( )
def set_remove ( self , save = True , reason = " " ) :
"""
Persist this IXFMemberData instance and send out notifications
for proposed removal of netixlan instance to ac , net and ix
as warranted
"""
self . reason = reason
# we perist this ix-f member data that proposes removal
# if any of these conditions are met
# marked for removal, but not saved
not_saved = not self . id and self . marked_for_removal
# was in remote-data last time, gone now
gone = (
self . id
and getattr ( self , " previous_data " , " {} " ) != " {} "
and self . remote_data_missing
)
if ( not_saved or gone ) and save :
self . set_data ( { } )
self . save ( )
2020-07-26 23:36:27 -05:00
return True
2020-07-15 02:07:01 -05:00
def set_data ( self , data ) :
"""
Stores a dict in self . data as a json string
"""
self . data = json . dumps ( data )
2020-07-26 23:36:27 -05:00
def render_notification ( self , template_file , recipient , context = None ) :
2020-07-15 02:07:01 -05:00
"""
2020-07-26 23:36:27 -05:00
Renders notification text for this ixfmemberdata
instance
2020-07-15 02:07:01 -05:00
Argument ( s ) :
- template_file ( str ) : email template file
- recipient ( str ) : ac , ix or net
- context ( dict ) : if set will update the template context
from this
"""
_context = {
" instance " : self ,
" recipient " : recipient ,
" ixf_url " : self . ixlan . ixf_ixp_member_list_url ,
}
if context :
_context . update ( context )
template = loader . get_template ( template_file )
2020-07-26 23:36:27 -05:00
return template . render ( _context )
2020-07-15 02:07:01 -05:00
@property
def ac_netixlan_url ( self ) :
if not self . netixlan . id :
return " "
path = django . urls . reverse (
" admin:peeringdb_server_networkixlan_change " , args = ( self . netixlan . id , ) ,
)
return f " { settings . BASE_URL } { path } "
@property
def ac_url ( self ) :
if not self . id :
return " "
path = django . urls . reverse (
" admin:peeringdb_server_ixfmemberdata_change " , args = ( self . id , ) ,
)
return f " { settings . BASE_URL } { path } "
# read only, or can make bigger, making smaller could break links
# validate could check
@reversion.register
class IXLanPrefix ( ProtectedMixin , pdb_models . IXLanPrefixBase ) :
"""
Descries a Prefix at an Exchange LAN
"""
ixlan = models . ForeignKey (
IXLan , on_delete = models . CASCADE , default = 0 , related_name = " ixpfx_set "
)
@property
def descriptive_name ( self ) :
"""
Returns a descriptive label of the ixpfx for logging purposes
"""
return f " ixpfx { self . id } { self . prefix } "
@classmethod
def nsp_namespace_from_id ( cls , org_id , ix_id , ixlan_id , id ) :
"""
Returns permissioning namespace for an ixpfx
"""
return " {} .prefix. {} " . format (
2020-07-26 23:36:27 -05:00
IXLan . nsp_namespace_from_id ( org_id , ix_id , ixlan_id ) , id ,
2020-07-15 02:07:01 -05:00
)
@classmethod
def related_to_ix ( cls , value = None , filt = None , field = " ix_id " , qset = None ) :
"""
Filter queryset of ixpfx objects related to exchange via ix_id match
according to filter
Relationship through ixlan - > ix
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( " ixlan__ %s " % field , filt , value )
return qset . filter ( * * filt )
@classmethod
def whereis_ip ( cls , ipaddr , qset = None ) :
"""
Filter queryset of ixpfx objects where the prefix contains
the supplied ipaddress
"""
if not qset :
qset = cls . handleref . undeleted ( )
ids = [ ]
ipaddr = ipaddress . ip_address ( ipaddr )
for ixpfx in qset :
if ipaddr in ixpfx . prefix :
ids . append ( ixpfx . id )
return qset . filter ( id__in = ids )
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this ixpfx
"""
return self . nsp_namespace_from_id (
self . ixlan . ix . org_id , self . ixlan . ix . id , self . ixlan . id , self . id
)
def __str__ ( self ) :
return f " { self . prefix } "
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " ixlan " ] )
def test_ip_address ( self , addr ) :
"""
Checks if this prefix can contain the specified ip address
Arguments :
- addr ( ipaddress . IPv4Address or ipaddress . IPv6Address or unicode ) : ip address
to check , can be either ipv4 or 6 but should be pre - validated to be in the
correct format as this function will simply return False incase of format
validation errors .
Returns :
- bool : True if prefix can contain the specified address
- bool : False if prefix cannot contain the specified address
"""
try :
if not addr :
return False
if isinstance ( addr , str ) :
addr = ipaddress . ip_address ( addr )
return addr in ipaddress . ip_network ( self . prefix )
except ipaddress . AddressValueError :
return False
except ValueError as inst :
return False
@property
def deletable ( self ) :
"""
Returns whether or not the prefix is currently
in a state where it can be marked as deleted .
This will be False for prefixes of which ANY
of the following is True :
- parent ixlan has netixlans that fall into
it ' s address space
"""
prefix = self . prefix
can_delete = True
for netixlan in self . ixlan . netixlan_set_active :
if self . protocol == " IPv4 " :
if netixlan . ipaddr4 and netixlan . ipaddr4 in prefix :
can_delete = False
break
if self . protocol == " IPv6 " :
if netixlan . ipaddr6 and netixlan . ipaddr6 in prefix :
can_delete = False
break
if not can_delete :
self . _not_deletable_reason = _ (
" There are active peers at this exchange that fall into "
" this address space "
)
else :
self . _not_deletable_reason = None
return can_delete
def clean ( self ) :
"""
Custom model validation
"""
status_error = _ (
" IXLanPrefix with status ' {} ' cannot be linked to a IXLan with status ' {} ' . "
) . format ( self . status , self . ixlan . status )
if self . ixlan . status == " pending " and self . status == " ok " :
raise ValidationError ( status_error )
elif self . ixlan . status == " deleted " and self . status in [ " ok " , " pending " ] :
raise ValidationError ( status_error )
# validate the specified prefix address
validate_address_space ( self . prefix )
validate_prefix_overlap ( self . prefix )
return super ( ) . clean ( )
@reversion.register
class Network ( pdb_models . NetworkBase ) :
"""
Describes a peeringdb network
"""
org = models . ForeignKey (
Organization , on_delete = models . CASCADE , related_name = " net_set "
)
allow_ixp_update = models . BooleanField (
null = False ,
default = False ,
help_text = _ (
2020-07-24 08:12:06 -07:00
" Sepcifies whether an IXP is allowed to add a netixlan entry for this network via their ixp_member data "
2020-07-15 02:07:01 -05:00
) ,
)
@staticmethod
def autocomplete_search_fields ( ) :
return (
" id__iexact " ,
" name__icontains " ,
)
def __unicode__ ( self ) :
return self . name
@classmethod
@reversion.create_revision ( )
def create_from_rdap ( cls , rdap , asn , org ) :
"""
Creates network from rdap result object
"""
name = rdap . name
if not rdap . name :
name = " AS %d " % ( asn )
if cls . objects . filter ( name = name ) . exists ( ) :
net = cls . objects . create ( org = org , asn = asn , name = " %s ! " % name , status = " ok " )
else :
net = cls . objects . create ( org = org , asn = asn , name = name , status = " ok " )
return net , True
@classmethod
def nsp_namespace_from_id ( cls , org_id , net_id ) :
2020-07-26 23:36:27 -05:00
return " {} .network. {} " . format (
Organization . nsp_namespace_from_id ( org_id ) , net_id
)
2020-07-15 02:07:01 -05:00
@classmethod
def related_to_fac ( cls , value = None , filt = None , field = " facility_id " , qset = None ) :
"""
Filter queryset of Network objects related to the facility
specified by fac_id
Relationship through netfac - > fac
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = NetworkFacility . handleref . filter ( * * filt )
return qset . filter ( id__in = [ i . network_id for i in q ] )
@classmethod
def not_related_to_fac ( cls , value = None , filt = None , field = " facility_id " , qset = None ) :
"""
Filter queryset of Network objects NOT related to the facility
specified by fac_id ( as in networks NOT present at the facility )
Relationship through netfac - > fac
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = NetworkFacility . handleref . filter ( * * filt )
return qset . exclude ( id__in = [ i . network_id for i in q ] )
2018-11-08 19:45:21 +00:00
@classmethod
def related_to_netfac ( cls , value = None , filt = None , field = " id " , qset = None ) :
"""
Filter queryset of Network objects related to the netfac link
specified by netfac_id
Relationship through netfac
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value , prefix = " netfac " )
q = NetworkFacility . handleref . filter ( * * filt )
return qset . filter ( id__in = [ i . network_id for i in q ] )
@classmethod
def related_to_netixlan ( cls , value = None , filt = None , field = " id " , qset = None ) :
"""
Filter queryset of Network objects related to the netixlan link
specified by netixlan_id
Relationship through netixlan
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value , prefix = " netixlan " )
q = NetworkIXLan . handleref . filter ( * * filt )
return qset . filter ( id__in = [ i . network_id for i in q ] )
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_ixlan ( cls , value = None , filt = None , field = " ixlan_id " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Filter queryset of Network objects related to the ixlan
specified by ixlan_id
Relationship through netixlan - > ixlan
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = NetworkIXLan . handleref . filter ( * * filt )
return qset . filter ( id__in = [ i . network_id for i in q ] )
@classmethod
def related_to_ix ( cls , value = None , filt = None , field = " ix_id " , qset = None ) :
"""
Filter queryset of Network objects related to the ix
specified by ix_id
Relationship through netixlan - > ixlan - > ix
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( " ixlan__ %s " % field , filt , value )
q = NetworkIXLan . handleref . select_related ( " ixlan " ) . filter ( * * filt )
return qset . filter ( id__in = [ i . network_id for i in q ] )
@classmethod
2019-12-05 16:57:52 +00:00
def not_related_to_ix ( cls , value = None , filt = None , field = " ix_id " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Filter queryset of Network objects not related to the ix
specified by ix_id ( as in networks not present at the exchange )
Relationship through netixlan - > ixlan - > ix
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( " ixlan__ %s " % field , filt , value )
q = NetworkIXLan . handleref . select_related ( " ixlan " ) . filter ( * * filt )
return qset . exclude ( id__in = [ i . network_id for i in q ] )
2019-05-02 18:13:03 +00:00
@classmethod
def as_set_map ( cls , qset = None ) :
"""
Returns a dict mapping asns to their irr_as_set value
"""
if not qset :
qset = cls . objects . filter ( status = " ok " ) . order_by ( " asn " )
2020-07-15 02:07:01 -05:00
return { net . asn : net . irr_as_set for net in qset }
2019-05-02 18:13:03 +00:00
2018-11-08 19:45:21 +00:00
@property
def search_result_name ( self ) :
"""
This will be the name displayed for quick search matches
of this entity
"""
2020-07-15 02:07:01 -05:00
return f " { self . name } ( { self . asn } ) "
2018-11-08 19:45:21 +00:00
@property
def netfac_set_active ( self ) :
return self . netfac_set ( manager = " handleref " ) . filter ( status = " ok " )
@property
def netixlan_set_active ( self ) :
return self . netixlan_set ( manager = " handleref " ) . filter ( status = " ok " )
2019-09-07 01:08:37 -05:00
@property
def ixlan_set_active ( self ) :
"""
Returns IXLan queryset for ixlans connected to this network
through NetworkIXLan
"""
ixlan_ids = [ ]
for netixlan in self . netixlan_set_active :
if netixlan . ixlan_id not in ixlan_ids :
ixlan_ids . append ( netixlan . ixlan_id )
return IXLan . objects . filter ( id__in = ixlan_ids )
@property
def ixlan_set_ixf_enabled ( self ) :
"""
Returns IXLan queryset for IX - F import enabled ixlans connected
to this network throught NetworkIXLan
"""
qset = self . ixlan_set_active . filter ( ixf_ixp_import_enabled = True )
qset = qset . exclude ( ixf_ixp_member_list_url__isnull = True )
return qset
2018-11-08 19:45:21 +00:00
@property
def poc_set_active ( self ) :
return self . poc_set ( manager = " handleref " ) . filter ( status = " ok " )
@property
def nsp_namespace ( self ) :
"""
Returns a custom permission namespace for an instance of this
model
"""
return self . __class__ . nsp_namespace_from_id ( self . org_id , self . id )
@property
def nsp_ruleset ( self ) :
"""
Ruleset to apply when applying permissions to the serialized
data of this model
"""
return {
# we require explicit perms to private network contacts
2019-12-05 16:57:52 +00:00
" require " : { " poc_set.users " : 0x01 , " poc_set.private " : 0x01 } ,
2018-11-08 19:45:21 +00:00
# since poc are stored in a list we need to specify a list
# handler for it, its a class function on NetworkContact that
# returns a relative permission namespace for each poc in the
# list
" list-handlers " : {
2019-12-05 16:57:52 +00:00
" poc_set " : { " namespace " : NetworkContact . nsp_namespace_in_list }
} ,
2018-11-08 19:45:21 +00:00
}
2020-07-26 23:36:27 -05:00
@property
def ipv4_support ( self ) :
# network has not indicated either ip4 or ip6 support
# so assume True (#771)
if not self . info_unicast and not self . info_ipv6 :
return True
return self . info_unicast
@property
def ipv6_support ( self ) :
# network has not indicated either ip4 or ip6 support
# so assume True (#771)
if not self . info_unicast and not self . info_ipv6 :
return True
return self . info_ipv6
2018-11-08 19:45:21 +00:00
@property
def sponsorship ( self ) :
return self . org . sponsorship
2020-04-03 13:36:44 +00:00
@property
def view_url ( self ) :
"""
Return the URL to this networks web view
"""
return " {} {} " . format (
settings . BASE_URL , django . urls . reverse ( " net-view " , args = ( self . id , ) )
)
2020-07-26 23:36:27 -05:00
@property
def view_url_asn ( self ) :
"""
Return the URL to this networks web view
"""
return " {} {} " . format (
settings . BASE_URL , django . urls . reverse ( " net-view-asn " , args = ( self . asn , ) )
)
2018-11-08 19:45:21 +00:00
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " org " ] )
def clean ( self ) :
"""
Custom model validation
"""
2020-03-31 11:04:26 +00:00
2018-11-08 19:45:21 +00:00
try :
validate_info_prefixes4 ( self . info_prefixes4 )
except ValidationError as exc :
2020-03-31 11:04:26 +00:00
raise ValidationError ( { " info_prefixes4 " : exc } )
2018-11-08 19:45:21 +00:00
try :
validate_info_prefixes6 ( self . info_prefixes6 )
except ValidationError as exc :
2020-03-31 11:04:26 +00:00
raise ValidationError ( { " info_prefixes6 " : exc } )
try :
if self . irr_as_set :
self . irr_as_set = validate_irr_as_set ( self . irr_as_set )
except ValidationError as exc :
raise ValidationError ( { " irr_as_set " : exc } )
2020-07-15 02:07:01 -05:00
return super ( ) . clean ( )
2018-11-08 19:45:21 +00:00
2020-03-31 11:04:26 +00:00
2018-11-08 19:45:21 +00:00
# class NetworkContact(HandleRefModel):
@reversion.register
class NetworkContact ( pdb_models . ContactBase ) :
"""
Describes a contact point ( phone , email etc . ) for a network
"""
# id = models.AutoField(primary_key=True)
2020-01-08 13:29:58 -06:00
network = models . ForeignKey (
Network , on_delete = models . CASCADE , default = 0 , related_name = " poc_set "
)
2018-11-08 19:45:21 +00:00
class Meta :
2020-01-08 13:29:58 -06:00
db_table = " peeringdb_network_contact "
2018-11-08 19:45:21 +00:00
@classmethod
def nsp_namespace_in_list ( cls , * * kwargs ) :
"""
This is used to build a relative namespace for this model if it is contained
within a list . The preceding namespace part will be provided by the container
element .
So in this case we just want to return the value of the visible attribute
"""
if " obj " in kwargs :
return " %s " % kwargs . get ( " obj " )
return kwargs . get ( " visible " )
@classmethod
def nsp_namespace_from_id ( cls , org_id , net_id , vis ) :
"""
Returns permissioning namespace for a network contact
"""
2020-07-26 23:36:27 -05:00
return " {} .poc_set. {} " . format (
Network . nsp_namespace_from_id ( org_id , net_id ) , vis
)
2018-11-08 19:45:21 +00:00
@property
def nsp_namespace ( self ) :
"""
Returns a custom namespace for an instance of this model
"""
return self . __class__ . nsp_namespace_from_id (
2019-12-05 16:57:52 +00:00
self . network . org_id , self . network . id , self . visible
)
2018-11-08 19:45:21 +00:00
@property
def nsp_require_explicit_read ( self ) :
"""
Make sure non - public instances of this models are always requiring
explicit permissions to view
"""
return self . visible != " Public "
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " net " ] )
2020-01-08 13:29:58 -06:00
def clean ( self ) :
self . phone = validate_phonenumber ( self . phone )
2018-11-08 19:45:21 +00:00
@reversion.register
class NetworkFacility ( pdb_models . NetworkFacilityBase ) :
"""
Describes a network < - > facility relationship
"""
2020-01-08 13:29:58 -06:00
network = models . ForeignKey (
Network , on_delete = models . CASCADE , default = 0 , related_name = " netfac_set "
)
facility = models . ForeignKey (
Facility , on_delete = models . CASCADE , default = 0 , related_name = " netfac_set "
)
2018-11-08 19:45:21 +00:00
class Meta :
2020-01-08 13:29:58 -06:00
db_table = " peeringdb_network_facility "
2019-12-05 16:57:52 +00:00
unique_together = ( " network " , " facility " , " local_asn " )
2019-02-15 17:46:20 +00:00
2018-11-08 19:45:21 +00:00
@classmethod
def nsp_namespace_from_id ( cls , org_id , net_id , fac_id ) :
"""
Returns permissioning namespace for a netfac
"""
2020-07-15 02:07:01 -05:00
return " {} .fac. {} " . format ( Network . nsp_namespace_from_id ( org_id , net_id ) , fac_id )
2018-11-08 19:45:21 +00:00
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this netfac
"""
return self . __class__ . nsp_namespace_from_id (
2019-12-05 16:57:52 +00:00
self . network . org_id , self . network_id , self . facility_id
)
2018-11-08 19:45:21 +00:00
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_name ( cls , value = None , filt = None , field = " facility__name " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Filter queryset of netfac objects related to facilities with name match
in facility__name according to filter
Relationship through facility
"""
if not qset :
qset = cls . handleref . undeleted ( )
return qset . filter ( * * make_relation_filter ( field , filt , value ) )
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_country (
cls , value = None , filt = None , field = " facility__country " , qset = None
) :
2018-11-08 19:45:21 +00:00
"""
Filter queryset of netfac objects related to country via match
in facility__country according to filter
Relationship through facility
"""
if not qset :
qset = cls . handleref . filter ( status = " ok " )
return qset . filter ( * * make_relation_filter ( field , filt , value ) )
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_city ( cls , value = None , filt = None , field = " facility__city " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Filter queryset of netfac objects related to city via match
in facility__city according to filter
Relationship through facility
"""
if not qset :
qset = cls . handleref . undeleted ( )
return qset . filter ( * * make_relation_filter ( field , filt , value ) )
2019-02-15 17:46:20 +00:00
@property
def descriptive_name ( self ) :
"""
Returns a descriptive label of the netfac for logging purposes
"""
return " netfac {} AS {} {} <-> {} " . format (
2019-12-05 16:57:52 +00:00
self . id , self . network . asn , self . network . name , self . facility . name
)
2019-02-15 17:46:20 +00:00
2018-11-08 19:45:21 +00:00
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " net " ] )
2020-01-08 13:29:58 -06:00
def clean ( self ) :
# when validating an existing netfac that has a mismatching
# local_asn value raise a validation error stating that it needs
# to be moved
#
# this is to catch and force correction of instances where they
# could not be migrated automatically during rollout of #168
# because the targeted local_asn did not exist in peeringdb
if self . id and self . local_asn != self . network . asn :
raise ValidationError (
_ (
" This entity was created for the ASN {} - please remove it from this network and recreate it under the correct network "
) . format ( self . local_asn )
)
# `local_asn` will eventually be dropped from the schema
# for now make sure it is always a match to the related
# network (#168)
self . local_asn = self . network . asn
2018-11-08 19:45:21 +00:00
# validate:
# ip in prefix
# prefix on lan
# FIXME - need unique constraint at save time, allow empty string for ipv4/ipv6
@reversion.register
class NetworkIXLan ( pdb_models . NetworkIXLanBase ) :
"""
Describes a network relationship to an IX through an IX Lan
"""
2020-01-08 13:29:58 -06:00
network = models . ForeignKey (
Network , on_delete = models . CASCADE , default = 0 , related_name = " netixlan_set "
)
ixlan = models . ForeignKey (
IXLan , on_delete = models . CASCADE , default = 0 , related_name = " netixlan_set "
)
2018-11-08 19:45:21 +00:00
class Meta :
2020-01-08 13:29:58 -06:00
db_table = " peeringdb_network_ixlan "
2020-07-15 02:07:01 -05:00
constraints = [
models . UniqueConstraint ( fields = [ " ipaddr4 " ] , name = " unique_ipaddr4 " ) ,
models . UniqueConstraint ( fields = [ " ipaddr6 " ] , name = " unique_ipaddr6 " ) ,
]
2018-11-08 19:45:21 +00:00
@property
def name ( self ) :
return " "
2019-02-15 17:46:20 +00:00
@property
def descriptive_name ( self ) :
"""
Returns a descriptive label of the netixlan for logging purposes
"""
return " netixlan {} AS {} {} {} " . format (
2019-12-05 16:57:52 +00:00
self . id , self . asn , self . ipaddr4 , self . ipaddr6
)
2019-02-15 17:46:20 +00:00
2018-11-08 19:45:21 +00:00
@property
def ix_name ( self ) :
"""
Returns the exchange name for this netixlan
"""
return self . ixlan . ix . name
@property
def ix_id ( self ) :
"""
Returns the exchange id for this netixlan
"""
return self . ixlan . ix_id
2020-07-15 02:07:01 -05:00
@property
def ixf_id ( self ) :
"""
Returns a tuple that identifies the netixlan
in the context of an ix - f member data entry
as a unqiue record by asn , ip4 and ip6 address
"""
2020-07-26 23:36:27 -05:00
net = self . network
2020-07-15 02:07:01 -05:00
return ( self . asn , self . ipaddr4 , self . ipaddr6 )
2020-07-26 23:36:27 -05:00
@property
def ixf_id_pretty_str ( self ) :
asn , ipaddr4 , ipaddr6 = self . ixf_id
ipaddr4 = ipaddr4 or _ ( " IPv4 not set " )
ipaddr6 = ipaddr6 or _ ( " IPv6 not set " )
return f " AS { asn } - { ipaddr4 } - { ipaddr6 } "
2018-11-08 19:45:21 +00:00
# FIXME
# permission namespacing
# right now it is assumed that the network owns the netixlan
# this needs to be discussed further
@classmethod
def nsp_namespace_from_id ( cls , org_id , net_id , ixlan_id ) :
"""
Returns permissioning namespace for a netixlan
"""
2020-07-26 23:36:27 -05:00
return " {} .ixlan. {} " . format (
Network . nsp_namespace_from_id ( org_id , net_id ) , ixlan_id
)
2018-11-08 19:45:21 +00:00
@property
def nsp_namespace ( self ) :
"""
Returns permissioning namespace for this netixlan
"""
return self . __class__ . nsp_namespace_from_id (
2019-12-05 16:57:52 +00:00
self . network . org_id , self . network . id , self . ixlan_id
)
2018-11-08 19:45:21 +00:00
def nsp_has_perms_PUT ( self , user , request ) :
return validate_PUT_ownership ( user , self , request . data , [ " net " ] )
@classmethod
def related_to_ix ( cls , value = None , filt = None , field = " ix_id " , qset = None ) :
"""
Filter queryset of netixlan objects related to the ix
specified by ix_id
Relationship through ixlan - > ix
"""
if not qset :
qset = cls . handleref . undeleted ( )
filt = make_relation_filter ( field , filt , value )
q = IXLan . handleref . select_related ( " ix " ) . filter ( * * filt )
return qset . filter ( ixlan_id__in = [ i . id for i in q ] )
@classmethod
2019-12-05 16:57:52 +00:00
def related_to_name ( cls , value = None , filt = None , field = " ix__name " , qset = None ) :
2018-11-08 19:45:21 +00:00
"""
Filter queryset of netixlan objects related to exchange via a name match
according to filter
Relationship through ixlan - > ix
"""
2019-12-05 16:57:52 +00:00
return cls . related_to_ix ( value = value , filt = filt , field = field , qset = qset )
2018-11-08 19:45:21 +00:00
def ipaddress_conflict ( self ) :
"""
Checks whether the ip addresses specified on this netixlan
exist on another netixlan ( with status = " ok " )
Returns :
- tuple ( bool , bool ) : tuple of two booleans , first boolean is
true if there was a conflict with the ip4 address , second
boolean is true if there was a conflict with the ip6
address
"""
2019-12-05 16:57:52 +00:00
ipv4 = NetworkIXLan . objects . filter ( ipaddr4 = self . ipaddr4 , status = " ok " ) . exclude (
id = self . id
)
ipv6 = NetworkIXLan . objects . filter ( ipaddr6 = self . ipaddr6 , status = " ok " ) . exclude (
id = self . id
)
conflict_v4 = self . ipaddr4 and ipv4 . exists ( )
conflict_v6 = self . ipaddr6 and ipv6 . exists ( )
2018-11-08 19:45:21 +00:00
return ( conflict_v4 , conflict_v6 )
2019-05-02 18:16:44 +00:00
def validate_ipaddr4 ( self ) :
if self . ipaddr4 and not self . ixlan . test_ipv4_address ( self . ipaddr4 ) :
raise ValidationError ( _ ( " IPv4 address outside of prefix " ) )
def validate_ipaddr6 ( self ) :
if self . ipaddr6 and not self . ixlan . test_ipv6_address ( self . ipaddr6 ) :
raise ValidationError ( _ ( " IPv6 address outside of prefix " ) )
2018-11-08 19:45:21 +00:00
def clean ( self ) :
"""
Custom model validation
"""
errors = { }
2019-05-02 18:16:44 +00:00
# check that the ip address can be validated agaisnt
# at least one of the prefix on the parent ixlan
try :
self . validate_ipaddr4 ( )
except ValidationError as exc :
errors [ " ipaddr4 " ] = exc . message
try :
self . validate_ipaddr6 ( )
except ValidationError as exc :
errors [ " ipaddr6 " ] = exc . message
if errors :
raise ValidationError ( errors )
# make sure this ip address is not claimed anywhere else
2018-11-08 19:45:21 +00:00
conflict_v4 , conflict_v6 = self . ipaddress_conflict ( )
if conflict_v4 :
errors [ " ipaddr4 " ] = _ ( " Ip address already exists elsewhere " )
if conflict_v6 :
errors [ " ipaddr6 " ] = _ ( " Ip address already exists elsewhere " )
if errors :
raise ValidationError ( errors )
2020-01-08 13:29:58 -06:00
# when validating an existing netixlan that has a mismatching
# asn value raise a validation error stating that it needs
# to be moved
#
# this is to catch and force correction of instances where they
# could not be migrated automatically during rollout of #168
# because the targeted asn did not exist in peeringdb
if self . id and self . asn != self . network . asn :
raise ValidationError (
_ (
" This entity was created for the ASN {} - please remove it from this network and recreate it under the correct network "
) . format ( self . asn )
)
# `asn` will eventually be dropped from the schema
# for now make sure it is always a match to the related
# network (#168)
self . asn = self . network . asn
2019-05-02 18:28:35 +00:00
def ipaddr ( self , version ) :
"""
Return the netixlan ' s ipaddr for ip version
"""
if version == 4 :
return self . ipaddr4
elif version == 6 :
return self . ipaddr6
2020-07-15 02:07:01 -05:00
raise ValueError ( f " Invalid ip version { version } " )
2019-05-02 18:28:35 +00:00
def descriptive_name_ipv ( self , version ) :
"""
Returns a descriptive label of the netixlan for logging purposes
Will only contain the ipaddress matching the specified version
"""
2019-12-05 16:57:52 +00:00
return " netixlan {} AS {} {} " . format ( self . id , self . asn , self . ipaddr ( version ) )
2019-05-02 18:28:35 +00:00
2018-11-08 19:45:21 +00:00
class User ( AbstractBaseUser , PermissionsMixin ) :
"""
proper length fields user
"""
2019-12-05 16:57:52 +00:00
2018-11-08 19:45:21 +00:00
username = models . CharField (
2019-12-05 16:57:52 +00:00
_ ( " username " ) ,
max_length = 254 ,
unique = True ,
help_text = _ ( " Required. Letters, digits and [@.+-/_=|] only. " ) ,
2018-11-08 19:45:21 +00:00
validators = [
2019-12-05 16:57:52 +00:00
validators . RegexValidator (
r " ^[ \ w \ .@+-=|/]+$ " ,
_ ( " Enter a valid username. " ) ,
" invalid " ,
flags = re . UNICODE ,
)
] ,
)
email = models . EmailField ( _ ( " email address " ) , max_length = 254 )
first_name = models . CharField ( _ ( " first name " ) , max_length = 254 , blank = True )
last_name = models . CharField ( _ ( " last name " ) , max_length = 254 , blank = True )
2018-11-08 19:45:21 +00:00
is_staff = models . BooleanField (
2019-12-05 16:57:52 +00:00
_ ( " staff status " ) ,
default = False ,
help_text = _ ( " Designates whether the user can log into admin site. " ) ,
)
2018-11-08 19:45:21 +00:00
is_active = models . BooleanField (
2019-12-05 16:57:52 +00:00
_ ( " active " ) ,
default = True ,
help_text = _ (
" Designates whether this user should be treated as active. Unselect this instead of deleting accounts. "
) ,
)
date_joined = models . DateTimeField ( _ ( " date joined " ) , default = timezone . now )
2018-11-08 19:45:21 +00:00
created = CreatedDateTimeField ( )
updated = UpdatedDateTimeField ( )
2019-12-05 16:57:52 +00:00
status = models . CharField ( _ ( " status " ) , max_length = 254 , default = " ok " )
locale = models . CharField ( _ ( " language " ) , max_length = 62 , blank = True , null = True )
2018-11-08 19:45:21 +00:00
objects = UserManager ( )
2019-12-05 16:57:52 +00:00
USERNAME_FIELD = " username "
REQUIRED_FIELDS = [ " email " ]
2018-11-08 19:45:21 +00:00
class Meta :
db_table = " peeringdb_user "
2019-12-05 16:57:52 +00:00
verbose_name = _ ( " user " )
verbose_name_plural = _ ( " users " )
2018-11-08 19:45:21 +00:00
2020-04-01 11:08:12 +00:00
@property
def pending_affiliation_requests ( self ) :
"""
Returns the currently pending user - > org affiliation
requests for this user
"""
return self . affiliation_requests . filter ( status = " pending " ) . order_by ( " -created " )
@property
def affiliation_requests_available ( self ) :
"""
Returns whether the user currently has any affiliation request
slots available , by checking that the number of pending affiliation requests
the user has is lower than MAX_USER_AFFILIATION_REQUESTS
"""
2020-07-15 02:07:01 -05:00
return (
self . pending_affiliation_requests . count ( )
< settings . MAX_USER_AFFILIATION_REQUESTS
)
2020-04-01 11:08:12 +00:00
2018-11-08 19:45:21 +00:00
@property
def organizations ( self ) :
"""
Returns all organizations this user is a member of
"""
ids = [ ]
for group in self . groups . all ( ) :
m = re . match ( r " ^org \ .( \ d+).*$ " , group . name )
if m and int ( m . group ( 1 ) ) not in ids :
ids . append ( int ( m . group ( 1 ) ) )
2019-12-05 16:57:52 +00:00
return [ org for org in Organization . objects . filter ( id__in = ids , status = " ok " ) ]
2018-11-08 19:45:21 +00:00
@property
def networks ( self ) :
"""
Returns all networks this user is a member of
"""
return list (
2019-12-05 16:57:52 +00:00
chain . from_iterable ( org . net_set_active . all ( ) for org in self . organizations )
)
2018-11-08 19:45:21 +00:00
@property
def full_name ( self ) :
2020-07-15 02:07:01 -05:00
return f " { self . first_name } { self . last_name } "
2018-11-08 19:45:21 +00:00
@property
def has_oauth ( self ) :
return SocialAccount . objects . filter ( user = self ) . count ( ) > 0
@property
def email_confirmed ( self ) :
"""
Returns True if the email specified by the user has
been confirmed , False if not
"""
try :
2019-12-05 16:57:52 +00:00
email = EmailAddress . objects . get ( user = self , email = self . email , primary = True )
2018-11-08 19:45:21 +00:00
except EmailAddress . DoesNotExist :
return False
return email . verified
@property
2020-06-24 12:55:01 -05:00
def is_verified_user ( self ) :
2018-11-08 19:45:21 +00:00
"""
Returns whether the user is verified ( eg has been validated
by pdb staff )
right now this is accomplished by checking if the user
has been added to the ' user ' user group
"""
group = Group . objects . get ( id = settings . USER_GROUP_ID )
return group in self . groups . all ( )
2020-04-10 15:00:27 +00:00
@staticmethod
def autocomplete_search_fields ( ) :
"""
Used by grappelli autocomplete to determine what
fields to search in
"""
return ( " username__icontains " , " email__icontains " , " last_name__icontains " )
def related_label ( self ) :
"""
Used by grappelli autocomplete for representation
"""
2020-07-15 02:07:01 -05:00
return f " { self . username } < { self . email } > ( { self . id } ) "
2020-04-10 15:00:27 +00:00
2020-04-01 11:08:12 +00:00
def flush_affiliation_requests ( self ) :
"""
Removes all user - > org affiliation requests for this user
that have been denied or canceled
"""
UserOrgAffiliationRequest . objects . filter (
2020-07-15 02:07:01 -05:00
user = self , status__in = [ " denied " , " canceled " ]
2020-04-01 11:08:12 +00:00
) . delete ( )
2018-11-08 19:45:21 +00:00
def get_locale ( self ) :
" Returns user preferred language. "
return self . locale
def set_locale ( self , locale ) :
" Returns user preferred language. "
self . locale = locale
self . save ( )
def get_absolute_url ( self ) :
return " /users/ %s / " % urlquote ( self . email )
def get_full_name ( self ) :
"""
Returns the first_name plus the last_name , with a space in between .
"""
2020-07-15 02:07:01 -05:00
full_name = f " { self . first_name } { self . last_name } "
2018-11-08 19:45:21 +00:00
return full_name . strip ( )
def get_short_name ( self ) :
" Returns the short name for the user. "
return self . first_name
2019-12-05 16:57:52 +00:00
def email_user ( self , subject , message , from_email = settings . DEFAULT_FROM_EMAIL ) :
2018-11-08 19:45:21 +00:00
"""
Sends an email to this User .
"""
if not getattr ( settings , " MAIL_DEBUG " , False ) :
mail = EmailMultiAlternatives (
2019-12-05 16:57:52 +00:00
subject ,
message ,
from_email ,
[ self . email ] ,
headers = { " Auto-Submitted " : " auto-generated " , " Return-Path " : " <> " } ,
)
2018-11-08 19:45:21 +00:00
mail . send ( fail_silently = False )
else :
debug_mail ( subject , message , from_email , [ self . email ] )
def set_unverified ( self ) :
"""
Remove user from ' user ' group
Add user to ' guest ' group
"""
guest_group = Group . objects . get ( id = settings . GUEST_GROUP_ID )
user_group = Group . objects . get ( id = settings . USER_GROUP_ID )
groups = self . groups . all ( )
if guest_group not in groups :
guest_group . user_set . add ( self )
if user_group in groups :
user_group . user_set . remove ( self )
self . status = " pending "
self . save ( )
def set_verified ( self ) :
"""
Add user to ' user ' group
Remove user from ' guest ' group
"""
guest_group = Group . objects . get ( id = settings . GUEST_GROUP_ID )
user_group = Group . objects . get ( id = settings . USER_GROUP_ID )
groups = self . groups . all ( )
if guest_group in groups :
guest_group . user_set . remove ( self )
if user_group not in groups :
user_group . user_set . add ( self )
self . status = " ok "
self . save ( )
def send_email_confirmation ( self , request = None , signup = False ) :
"""
Use allauth email - confirmation process to make user
confirm that the email they provided is theirs .
"""
# check if there is actually an email set on the user
if not self . email :
return None
# allauth supports multiple email addresses per user, however
# we dont need that, so we check for the primary email address
# and if it already exist we make sure to update it to the
# email address currently specified on the user instance
try :
email = EmailAddress . objects . get ( email = self . email )
email . email = self . email
email . user = self
email . verified = False
try :
EmailConfirmation . objects . get ( email_address = email ) . delete ( )
except EmailConfirmation . DoesNotExist :
pass
except EmailAddress . DoesNotExist :
if EmailAddress . objects . filter ( user = self ) . exists ( ) :
EmailAddress . objects . filter ( user = self ) . delete ( )
email = EmailAddress ( user = self , email = self . email , primary = True )
email . save ( )
email . send_confirmation ( request = request , signup = signup )
return email
def password_reset_complete ( self , token , password ) :
if self . password_reset . match ( token ) :
self . set_password ( password )
self . save ( )
self . password_reset . delete ( )
def password_reset_initiate ( self ) :
"""
Initiate the password reset process for the user
"""
2019-12-05 16:57:52 +00:00
# pylint: disable=access-member-before-definition
2018-11-08 19:45:21 +00:00
if self . id :
try :
self . password_reset . delete ( )
except UserPasswordReset . DoesNotExist :
pass
token , hashed = password_reset_token ( )
self . password_reset = UserPasswordReset . objects . create (
2019-12-05 16:57:52 +00:00
user = self , token = hashed
)
2018-11-08 19:45:21 +00:00
template = loader . get_template ( " email/password-reset.txt " )
with override ( self . locale ) :
self . email_user (
_ ( " Password Reset Initiated " ) ,
2019-12-05 16:57:52 +00:00
template . render (
{
" user " : self ,
" token " : token ,
" password_reset_url " : settings . PASSWORD_RESET_URL ,
}
) ,
)
2018-11-08 19:45:21 +00:00
return token , hashed
return None , None
def vq_approve ( self ) :
self . set_verified ( )
def is_org_member ( self , org ) :
return self . groups . filter ( id = org . usergroup . id ) . exists ( )
def is_org_admin ( self , org ) :
return self . groups . filter ( id = org . admin_usergroup . id ) . exists ( )
def validate_rdap_relationship ( self , rdap ) :
"""
#Domain only matching
email_domain = self . email . split ( " @ " ) [ 1 ]
for email in rdap . emails :
try :
domain = email . split ( " @ " ) [ 1 ]
if email_domain == domain :
return True
except IndexError , inst :
pass
"""
# Exact email matching
for email in rdap . emails :
if email . lower ( ) == self . email . lower ( ) :
return True
return False
def password_reset_token ( ) :
token = str ( uuid . uuid4 ( ) )
2020-01-08 13:29:58 -06:00
hashed = sha256_crypt . hash ( token )
2018-11-08 19:45:21 +00:00
return token , hashed
2020-07-26 23:36:27 -05:00
class IXFImportEmail ( models . Model ) :
"""
A copy of all emails sent by the IX - F importer .
"""
subject = models . CharField ( max_length = 255 , blank = False )
message = models . TextField ( blank = False )
recipients = models . CharField ( max_length = 255 , blank = False )
created = models . DateTimeField ( auto_now_add = True )
sent = models . DateTimeField ( blank = True , null = True )
net = models . ForeignKey (
Network ,
on_delete = models . CASCADE ,
related_name = " network_email_set " ,
blank = True ,
null = True ,
)
ix = models . ForeignKey (
InternetExchange ,
on_delete = models . CASCADE ,
related_name = " ix_email_set " ,
blank = True ,
null = True ,
)
class Meta :
verbose_name = _ ( " IXF Import Email " )
verbose_name_plural = _ ( " IXF Import Emails " )
2018-11-08 19:45:21 +00:00
class UserPasswordReset ( models . Model ) :
class Meta :
db_table = " peeringdb_user_password_reset "
2020-01-08 13:29:58 -06:00
user = models . OneToOneField (
User , on_delete = models . CASCADE , primary_key = True , related_name = " password_reset "
)
2018-11-08 19:45:21 +00:00
token = models . CharField ( max_length = 255 )
created = models . DateTimeField ( auto_now_add = True )
def is_valid ( self ) :
valid_until = self . created + datetime . timedelta ( hours = 2 )
if datetime . datetime . now ( ) . replace ( tzinfo = UTC ( ) ) > valid_until :
return False
return True
def match ( self , token ) :
return sha256_crypt . verify ( token , self . token )
class CommandLineTool ( models . Model ) :
"""
Describes command line tool execution by a staff user inside the
control panel ( admin )
"""
2019-12-05 16:57:52 +00:00
tool = models . CharField (
max_length = 255 , help_text = _ ( " name of the tool " ) , choices = COMMANDLINE_TOOLS
)
2018-11-08 19:45:21 +00:00
arguments = models . TextField (
2019-12-05 16:57:52 +00:00
help_text = _ ( " json serialization of arguments and options passed " )
)
2018-11-08 19:45:21 +00:00
result = models . TextField ( null = True , blank = True , help_text = _ ( " result log " ) )
description = models . CharField (
max_length = 255 ,
help_text = _ ( " Descriptive text of command that can be searched " ) ,
2019-12-05 16:57:52 +00:00
null = True ,
blank = True ,
)
user = models . ForeignKey (
2020-01-08 13:29:58 -06:00
User ,
on_delete = models . CASCADE ,
help_text = _ ( " the user that ran this command " ) ,
related_name = " clt_history " ,
2019-12-05 16:57:52 +00:00
)
2018-11-08 19:45:21 +00:00
created = models . DateTimeField (
2019-12-05 16:57:52 +00:00
auto_now_add = True , help_text = _ ( " command was run at this date and time " )
)
2018-11-08 19:45:21 +00:00
2019-12-05 16:57:52 +00:00
status = models . CharField (
max_length = 255 ,
default = " done " ,
choices = [
( " done " , _ ( " Done " ) ) ,
( " waiting " , _ ( " Waiting " ) ) ,
( " running " , _ ( " Running " ) ) ,
] ,
)
2019-01-04 10:02:28 +00:00
2018-11-08 19:45:21 +00:00
def __str__ ( self ) :
2020-07-15 02:07:01 -05:00
return f " { self . tool } : { self . description } "
2018-11-08 19:45:21 +00:00
2019-01-04 10:02:28 +00:00
def set_waiting ( self ) :
self . status = " waiting "
def set_done ( self ) :
self . status = " done "
def set_running ( self ) :
self . status = " running "
2020-07-26 23:36:27 -05:00
class EnvironmentSetting ( models . Model ) :
"""
Environment settings overrides controlled through
django admin ( / cp )
"""
class Meta :
db_table = " peeringdb_settings "
verbose_name = _ ( " Environment Setting " )
verbose_name_plural = _ ( " Environment Settings " )
setting = models . CharField (
max_length = 255 ,
choices = (
(
" IXF_IMPORTER_DAYS_UNTIL_TICKET " ,
_ ( " IX-F Importer: Days until DeskPRO ticket is created " ) ,
) ,
) ,
unique = True ,
)
value_str = models . CharField ( max_length = 255 , blank = True , null = True )
value_int = models . IntegerField ( blank = True , null = True )
value_bool = models . BooleanField ( blank = True , default = False )
value_float = models . FloatField ( blank = True , null = True )
updated = models . DateTimeField (
_ ( " Last Updated " ) , auto_now = True , null = True , blank = True ,
)
created = models . DateTimeField (
_ ( " Configured on " ) , auto_now_add = True , blank = True , null = True ,
)
user = models . ForeignKey (
User ,
null = True ,
on_delete = models . SET_NULL ,
related_name = " admincom_setting_set " ,
help_text = _ ( " Last updated by this user " ) ,
)
setting_to_field = {
" IXF_IMPORTER_DAYS_UNTIL_TICKET " : " value_int " ,
}
@classmethod
def get_setting_value ( cls , setting ) :
"""
Get the current value of the setting specified by
it ' s setting name
If no instance has been saved for the specified setting
the default value will be returned
"""
try :
instance = cls . objects . get ( setting = setting )
return instance . value
except cls . DoesNotExist :
return getattr ( settings , setting )
@property
def value ( self ) :
"""
Get the value for this setting
"""
return getattr ( self , self . setting_to_field [ self . setting ] )
def set_value ( self , value ) :
"""
Update the value for this setting
"""
setattr ( self , self . setting_to_field [ self . setting ] , value )
self . full_clean ( )
self . save ( )
2020-07-15 02:07:01 -05:00
REFTAG_MAP = {
2020-07-26 23:36:27 -05:00
cls . handleref . tag : cls
for cls in [
Organization ,
Network ,
Facility ,
InternetExchange ,
InternetExchangeFacility ,
NetworkFacility ,
NetworkIXLan ,
NetworkContact ,
IXLan ,
IXLanPrefix ,
]
2020-07-15 02:07:01 -05:00
}
2018-11-08 19:45:21 +00:00
2019-01-04 10:02:28 +00:00
QUEUE_ENABLED = [ ]
QUEUE_NOTIFY = [ ]
if not getattr ( settings , " DISABLE_VERIFICATION_QUEUE " , False ) :
# enable verification queue for these models
QUEUE_ENABLED = ( User , InternetExchange , Network , Facility , Organization )
if not getattr ( settings , " DISABLE_VERIFICATION_QUEUE_EMAILS " , False ) :
# send admin notification emails for these models
QUEUE_NOTIFY = ( InternetExchange , Network , Facility , Organization )
2018-11-08 19:45:21 +00:00
autodiscover_namespaces ( Network , Facility , InternetExchange )