diff --git a/docs/data-model/tenancy.md b/docs/data-model/tenancy.md index d3e8b8c29..c9cfc997c 100644 --- a/docs/data-model/tenancy.md +++ b/docs/data-model/tenancy.md @@ -4,6 +4,19 @@ NetBox supports the concept of individual tenants within its parent organization A tenant represents a discrete organization. Certain resources within NetBox can be assigned to a tenant. This makes it very convenient to track which resources are assigned to which customers, for instance. +The following objects can be assigned to tenants: + +* Sites +* Racks +* Devices +* VRFs +* Prefixes +* IP addresses +* VLANs +* Circuits + +If a prefix or IP address is not assigned to a tenant, it will appear to inherit the tenant to which its parent VRF is assigned, if any. + ### Tenant Groups -Tenants are grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions." +Tenants can be grouped by type. For instance, you might create one group called "Customers" and one called "Acquisitions." The assignment of tenants to groups is optional. diff --git a/netbox/circuits/tables.py b/netbox/circuits/tables.py index 66cbd33ca..f82459890 100644 --- a/netbox/circuits/tables.py +++ b/netbox/circuits/tables.py @@ -57,9 +57,11 @@ class CircuitTable(BaseTable): provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider') tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') - port_speed_human = tables.Column(verbose_name='Port Speed') - commit_rate_human = tables.Column(verbose_name='Commit Rate') + port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'), + verbose_name='Port Speed') + commit_rate = tables.Column(accessor=Accessor('commit_rate_human'), order_by=Accessor('commit_rate'), + verbose_name='Commit Rate') class Meta(BaseTable.Meta): model = Circuit - fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed_human', 'commit_rate_human') + fields = ('pk', 'cid', 'type', 'provider', 'tenant', 'site', 'port_speed', 'commit_rate') diff --git a/netbox/ipam/forms.py b/netbox/ipam/forms.py index 0449efdb2..134c2933c 100644 --- a/netbox/ipam/forms.py +++ b/netbox/ipam/forms.py @@ -270,6 +270,11 @@ def prefix_vrf_choices(): return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in vrf_choices] +def tenant_choices(): + tenant_choices = Tenant.objects.all() + return [(t.slug, t.name) for t in tenant_choices] + + def prefix_site_choices(): site_choices = Site.objects.annotate(prefix_count=Count('prefixes')) return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices] @@ -291,8 +296,8 @@ class PrefixFilterForm(forms.Form, BootstrapMixin): parent = forms.CharField(required=False, label='Search Within') vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF', widget=forms.SelectMultiple(attrs={'size': 6})) - tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False, - widget=forms.SelectMultiple(attrs={'size': 6})) + tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant', + widget=forms.SelectMultiple(attrs={'size': 6})) status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices, widget=forms.SelectMultiple(attrs={'size': 6})) site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices, @@ -442,8 +447,8 @@ class IPAddressFilterForm(forms.Form, BootstrapMixin): family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family') vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF', widget=forms.SelectMultiple(attrs={'size': 6})) - tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False, - widget=forms.SelectMultiple(attrs={'size': 6})) + tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant', + widget=forms.SelectMultiple(attrs={'size': 6})) # diff --git a/netbox/ipam/tables.py b/netbox/ipam/tables.py index a7444b6b8..4c5bbee3a 100644 --- a/netbox/ipam/tables.py +++ b/netbox/ipam/tables.py @@ -39,6 +39,16 @@ PREFIX_LINK_BRIEF = """ """ +IPADDRESS_LINK = """ +{% if record.pk %} + {{ record.address }} +{% elif perms.ipam.add_ipaddress %} + {% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }} +{% else %} + {{ record.0 }} +{% endif %} +""" + STATUS_LABEL = """ {% if record.pk %} {{ record.get_status_display }} @@ -148,17 +158,21 @@ class PrefixTable(BaseTable): class Meta(BaseTable.Meta): model = Prefix fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description') + row_attrs = { + 'class': lambda record: 'success' if not record.pk else '', + } class PrefixBriefTable(BaseTable): prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix') + vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site') status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status') role = tables.Column(verbose_name='Role') class Meta(BaseTable.Meta): model = Prefix - fields = ('prefix', 'status', 'site', 'role') + fields = ('prefix', 'vrf', 'status', 'site', 'role') orderable = False @@ -168,7 +182,7 @@ class PrefixBriefTable(BaseTable): class IPAddressTable(BaseTable): pk = ToggleColumn() - address = tables.LinkColumn('ipam:ipaddress', args=[Accessor('pk')], verbose_name='IP Address') + address = tables.TemplateColumn(IPADDRESS_LINK, verbose_name='IP Address') vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF') tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant') device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False, @@ -179,6 +193,9 @@ class IPAddressTable(BaseTable): class Meta(BaseTable.Meta): model = IPAddress fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description') + row_attrs = { + 'class': lambda record: 'success' if not isinstance(record, IPAddress) else '', + } class IPAddressBriefTable(BaseTable): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index dbb280c61..ce3c2a8f9 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -1,8 +1,8 @@ -from netaddr import IPSet +import netaddr from django_tables2 import RequestConfig from django.contrib.auth.mixins import PermissionRequiredMixin -from django.db.models import Count +from django.db.models import Count, Q from django.shortcuts import get_object_or_404, render from dcim.models import Device @@ -21,7 +21,7 @@ def add_available_prefixes(parent, prefix_list): """ # Find all unallocated space - available_prefixes = IPSet(parent) ^ IPSet([p.prefix for p in prefix_list]) + available_prefixes = netaddr.IPSet(parent) ^ netaddr.IPSet([p.prefix for p in prefix_list]) available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()] # Concatenate and sort complete list of children @@ -31,6 +31,57 @@ def add_available_prefixes(parent, prefix_list): return prefix_list +def add_available_ipaddresses(prefix, ipaddress_list): + """ + Annotate ranges of available IP addresses within a given prefix. + """ + + output = [] + prev_ip = None + + # Ignore the "network address" for IPv4 prefixes larger than /31 + if prefix.version == 4 and prefix.prefixlen < 31: + first_ip_in_prefix = netaddr.IPAddress(prefix.first + 1) + else: + first_ip_in_prefix = netaddr.IPAddress(prefix.first) + + # Ignore the broadcast address for IPv4 prefixes larger than /31 + if prefix.version == 4 and prefix.prefixlen < 31: + last_ip_in_prefix = netaddr.IPAddress(prefix.last - 1) + else: + last_ip_in_prefix = netaddr.IPAddress(prefix.last) + + if not ipaddress_list: + return [( + int(last_ip_in_prefix - first_ip_in_prefix + 1), + '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen) + )] + + # Account for any available IPs before the first real IP + if ipaddress_list[0].address.ip > first_ip_in_prefix: + skipped_count = int(ipaddress_list[0].address.ip - first_ip_in_prefix) + first_skipped = '{}/{}'.format(first_ip_in_prefix, prefix.prefixlen) + output.append((skipped_count, first_skipped)) + + # Iterate through existing IPs and annotate free ranges + for ip in ipaddress_list: + if prev_ip: + skipped_count = int(ip.address.ip - prev_ip.address.ip - 1) + if skipped_count: + first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen) + output.append((skipped_count, first_skipped)) + output.append(ip) + prev_ip = ip + + # Include any remaining available IPs + if prev_ip.address.ip < last_ip_in_prefix: + skipped_count = int(last_ip_in_prefix - prev_ip.address.ip) + first_skipped = '{}/{}'.format(prev_ip.address.ip + 1, prefix.prefixlen) + output.append((skipped_count, first_skipped)) + + return output + + # # VRFs # @@ -281,7 +332,8 @@ def prefix(request, pk): .count() # Parent prefixes table - parent_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contains=str(prefix.prefix))\ + parent_prefixes = Prefix.objects.filter(Q(vrf=prefix.vrf) | Q(vrf__isnull=True))\ + .filter(prefix__net_contains=str(prefix.prefix))\ .select_related('site', 'role').annotate_depth() parent_prefix_table = tables.PrefixBriefTable(parent_prefixes) @@ -291,7 +343,13 @@ def prefix(request, pk): duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes) # Child prefixes table - child_prefixes = Prefix.objects.filter(vrf=prefix.vrf, prefix__net_contained=str(prefix.prefix))\ + if prefix.vrf: + # If the prefix is in a VRF, show child prefixes only within that VRF. + child_prefixes = Prefix.objects.filter(vrf=prefix.vrf) + else: + # If the prefix is in the global table, show child prefixes from all VRFs. + child_prefixes = Prefix.objects.all() + child_prefixes = child_prefixes.filter(prefix__net_contained=str(prefix.prefix))\ .select_related('site', 'role').annotate_depth(limit=0) if child_prefixes: child_prefixes = add_available_prefixes(prefix.prefix, child_prefixes) @@ -368,6 +426,7 @@ def prefix_ipaddresses(request, pk): # Find all IPAddresses belonging to this Prefix ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.prefix))\ .select_related('vrf', 'interface__device', 'primary_ip4_for', 'primary_ip6_for') + ipaddresses = add_available_ipaddresses(prefix.prefix, ipaddresses) ip_table = tables.IPAddressTable(ipaddresses) ip_table.model = IPAddress diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 3ddbc8af5..4c5abcd56 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -12,7 +12,7 @@ except ImportError: "the documentation.") -VERSION = '1.4.0' +VERSION = '1.4.1' # Import local configuration for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']: diff --git a/netbox/templates/secrets/secretrole_list.html b/netbox/templates/secrets/secretrole_list.html index 55e12a1a8..4b1ee0399 100644 --- a/netbox/templates/secrets/secretrole_list.html +++ b/netbox/templates/secrets/secretrole_list.html @@ -12,7 +12,7 @@ {% endif %} -