mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-23 17:08:41 -06:00
commit
946a1b751b
@ -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.
|
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
|
### 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.
|
||||||
|
@ -57,9 +57,11 @@ class CircuitTable(BaseTable):
|
|||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')], verbose_name='Provider')
|
||||||
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
tenant = tables.LinkColumn('tenancy:tenant', args=[Accessor('tenant.slug')], verbose_name='Tenant')
|
||||||
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
port_speed_human = tables.Column(verbose_name='Port Speed')
|
port_speed = tables.Column(accessor=Accessor('port_speed_human'), order_by=Accessor('port_speed'),
|
||||||
commit_rate_human = tables.Column(verbose_name='Commit Rate')
|
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):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
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')
|
||||||
|
@ -270,6 +270,11 @@ def prefix_vrf_choices():
|
|||||||
return [(v.pk, u'{} ({})'.format(v.name, v.prefix_count)) for v in 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():
|
def prefix_site_choices():
|
||||||
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
site_choices = Site.objects.annotate(prefix_count=Count('prefixes'))
|
||||||
return [(s.slug, u'{} ({})'.format(s.name, s.prefix_count)) for s in site_choices]
|
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')
|
parent = forms.CharField(required=False, label='Search Within')
|
||||||
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
vrf = forms.MultipleChoiceField(required=False, choices=prefix_vrf_choices, label='VRF',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False,
|
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
|
status = forms.MultipleChoiceField(required=False, choices=prefix_status_choices,
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
site = forms.MultipleChoiceField(required=False, choices=prefix_site_choices,
|
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')
|
family = forms.ChoiceField(required=False, choices=ipaddress_family_choices, label='Address Family')
|
||||||
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
vrf = forms.MultipleChoiceField(required=False, choices=ipaddress_vrf_choices, label='VRF',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
tenant = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), required=False,
|
tenant = forms.MultipleChoiceField(required=False, choices=tenant_choices, label='Tenant',
|
||||||
widget=forms.SelectMultiple(attrs={'size': 6}))
|
widget=forms.SelectMultiple(attrs={'size': 6}))
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -39,6 +39,16 @@ PREFIX_LINK_BRIEF = """
|
|||||||
</span>
|
</span>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
IPADDRESS_LINK = """
|
||||||
|
{% if record.pk %}
|
||||||
|
<a href="{{ record.get_absolute_url }}">{{ record.address }}</a>
|
||||||
|
{% elif perms.ipam.add_ipaddress %}
|
||||||
|
<a href="{% url 'ipam:ipaddress_add' %}?address={{ record.1 }}" class="btn btn-xs btn-success">{% if record.0 <= 65536 %}{{ record.0 }}{% else %}Lots of{% endif %} free IP{{ record.0|pluralize }}</a>
|
||||||
|
{% else %}
|
||||||
|
{{ record.0 }}
|
||||||
|
{% endif %}
|
||||||
|
"""
|
||||||
|
|
||||||
STATUS_LABEL = """
|
STATUS_LABEL = """
|
||||||
{% if record.pk %}
|
{% if record.pk %}
|
||||||
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
<span class="label label-{{ record.get_status_class }}">{{ record.get_status_display }}</span>
|
||||||
@ -148,17 +158,21 @@ class PrefixTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
fields = ('pk', 'prefix', 'status', 'vrf', 'tenant', 'site', 'role', 'description')
|
||||||
|
row_attrs = {
|
||||||
|
'class': lambda record: 'success' if not record.pk else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class PrefixBriefTable(BaseTable):
|
class PrefixBriefTable(BaseTable):
|
||||||
prefix = tables.TemplateColumn(PREFIX_LINK_BRIEF, verbose_name='Prefix')
|
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')
|
site = tables.LinkColumn('dcim:site', args=[Accessor('site.slug')], verbose_name='Site')
|
||||||
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
status = tables.TemplateColumn(STATUS_LABEL, verbose_name='Status')
|
||||||
role = tables.Column(verbose_name='Role')
|
role = tables.Column(verbose_name='Role')
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Prefix
|
model = Prefix
|
||||||
fields = ('prefix', 'status', 'site', 'role')
|
fields = ('prefix', 'vrf', 'status', 'site', 'role')
|
||||||
orderable = False
|
orderable = False
|
||||||
|
|
||||||
|
|
||||||
@ -168,7 +182,7 @@ class PrefixBriefTable(BaseTable):
|
|||||||
|
|
||||||
class IPAddressTable(BaseTable):
|
class IPAddressTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
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')
|
vrf = tables.LinkColumn('ipam:vrf', args=[Accessor('vrf.pk')], default='Global', verbose_name='VRF')
|
||||||
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
tenant = tables.TemplateColumn(TENANT_LINK, verbose_name='Tenant')
|
||||||
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
device = tables.LinkColumn('dcim:device', args=[Accessor('interface.device.pk')], orderable=False,
|
||||||
@ -179,6 +193,9 @@ class IPAddressTable(BaseTable):
|
|||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = IPAddress
|
model = IPAddress
|
||||||
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
|
fields = ('pk', 'address', 'vrf', 'tenant', 'device', 'interface', 'description')
|
||||||
|
row_attrs = {
|
||||||
|
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class IPAddressBriefTable(BaseTable):
|
class IPAddressBriefTable(BaseTable):
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
from netaddr import IPSet
|
import netaddr
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
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 django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
from dcim.models import Device
|
from dcim.models import Device
|
||||||
@ -21,7 +21,7 @@ def add_available_prefixes(parent, prefix_list):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
# Find all unallocated space
|
# 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()]
|
available_prefixes = [Prefix(prefix=p) for p in available_prefixes.iter_cidrs()]
|
||||||
|
|
||||||
# Concatenate and sort complete list of children
|
# Concatenate and sort complete list of children
|
||||||
@ -31,6 +31,57 @@ def add_available_prefixes(parent, prefix_list):
|
|||||||
return 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
|
# VRFs
|
||||||
#
|
#
|
||||||
@ -281,7 +332,8 @@ def prefix(request, pk):
|
|||||||
.count()
|
.count()
|
||||||
|
|
||||||
# Parent prefixes table
|
# 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()
|
.select_related('site', 'role').annotate_depth()
|
||||||
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
parent_prefix_table = tables.PrefixBriefTable(parent_prefixes)
|
||||||
|
|
||||||
@ -291,7 +343,13 @@ def prefix(request, pk):
|
|||||||
duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
|
duplicate_prefix_table = tables.PrefixBriefTable(duplicate_prefixes)
|
||||||
|
|
||||||
# Child prefixes table
|
# 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)
|
.select_related('site', 'role').annotate_depth(limit=0)
|
||||||
if child_prefixes:
|
if child_prefixes:
|
||||||
child_prefixes = add_available_prefixes(prefix.prefix, 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
|
# Find all IPAddresses belonging to this Prefix
|
||||||
ipaddresses = IPAddress.objects.filter(vrf=prefix.vrf, address__net_contained_or_equal=str(prefix.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')
|
.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 = tables.IPAddressTable(ipaddresses)
|
||||||
ip_table.model = IPAddress
|
ip_table.model = IPAddress
|
||||||
|
@ -12,7 +12,7 @@ except ImportError:
|
|||||||
"the documentation.")
|
"the documentation.")
|
||||||
|
|
||||||
|
|
||||||
VERSION = '1.4.0'
|
VERSION = '1.4.1'
|
||||||
|
|
||||||
# Import local configuration
|
# Import local configuration
|
||||||
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
for setting in ['ALLOWED_HOSTS', 'DATABASE', 'SECRET_KEY']:
|
||||||
|
@ -12,7 +12,7 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<h1>Device Roles</h1>
|
<h1>Secret Roles</h1>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
{% include 'utilities/obj_table.html' with bulk_delete_url='secrets:secretrole_bulk_delete' %}
|
{% include 'utilities/obj_table.html' with bulk_delete_url='secrets:secretrole_bulk_delete' %}
|
||||||
|
@ -93,35 +93,35 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row panel-body">
|
<div class="row panel-body">
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'dcim:site_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.site_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.site_count }}</a></h2>
|
<h2><a href="{% url 'dcim:site_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.site_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.site_count }}</a></h2>
|
||||||
<p>Sites</p>
|
<p>Sites</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'dcim:rack_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.rack_count }}</a></h2>
|
<h2><a href="{% url 'dcim:rack_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.rack_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.rack_count }}</a></h2>
|
||||||
<p>Racks</p>
|
<p>Racks</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'dcim:device_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.device_count }}</a></h2>
|
<h2><a href="{% url 'dcim:device_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.device_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.device_count }}</a></h2>
|
||||||
<p>Devices</p>
|
<p>Devices</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:vrf_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.vrf_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.vrf_count }}</a></h2>
|
<h2><a href="{% url 'ipam:vrf_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.vrf_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2>
|
||||||
<p>VRFs</p>
|
<p>VRFs</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:prefix_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.prefix_count }}</a></h2>
|
<h2><a href="{% url 'ipam:prefix_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.prefix_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
|
||||||
<p>Prefixes</p>
|
<p>Prefixes</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:ipaddress_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.ipaddress_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.ipaddress_count }}</a></h2>
|
<h2><a href="{% url 'ipam:ipaddress_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.ipaddress_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.ipaddress_count }}</a></h2>
|
||||||
<p>IP addresses</p>
|
<p>IP addresses</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'ipam:vlan_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.vlan_count }}</a></h2>
|
<h2><a href="{% url 'ipam:vlan_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.vlan_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.vlan_count }}</a></h2>
|
||||||
<p>VLANs</p>
|
<p>VLANs</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-4 text-center">
|
<div class="col-md-4 text-center">
|
||||||
<h2><a href="{% url 'circuits:circuit_list' %}?tenant={{ tenant.slug }}" class="btn {% if tenant.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ tenant.circuit_count }}</a></h2>
|
<h2><a href="{% url 'circuits:circuit_list' %}?tenant={{ tenant.slug }}" class="btn {% if stats.circuit_count %}btn-primary{% else %}btn-default{% endif %} btn-lg">{{ stats.circuit_count }}</a></h2>
|
||||||
<p>Circuits</p>
|
<p>Circuits</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -8,6 +8,18 @@ from utilities.forms import (
|
|||||||
from .models import Tenant, TenantGroup
|
from .models import Tenant, TenantGroup
|
||||||
|
|
||||||
|
|
||||||
|
def bulkedit_tenantgroup_choices():
|
||||||
|
"""
|
||||||
|
Include an option to remove the currently assigned TenantGroup from a Tenant.
|
||||||
|
"""
|
||||||
|
choices = [
|
||||||
|
(None, '---------'),
|
||||||
|
(0, 'None'),
|
||||||
|
]
|
||||||
|
choices += [(g.pk, g.name) for g in TenantGroup.objects.all()]
|
||||||
|
return choices
|
||||||
|
|
||||||
|
|
||||||
def bulkedit_tenant_choices():
|
def bulkedit_tenant_choices():
|
||||||
"""
|
"""
|
||||||
Include an option to remove the currently assigned Tenant from an object.
|
Include an option to remove the currently assigned Tenant from an object.
|
||||||
@ -46,7 +58,7 @@ class TenantForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
|
|
||||||
class TenantFromCSVForm(forms.ModelForm):
|
class TenantFromCSVForm(forms.ModelForm):
|
||||||
group = forms.ModelChoiceField(TenantGroup.objects.all(), to_field_name='name',
|
group = forms.ModelChoiceField(TenantGroup.objects.all(), required=False, to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Group not found.'})
|
error_messages={'invalid_choice': 'Group not found.'})
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -60,7 +72,7 @@ class TenantImportForm(BulkImportForm, BootstrapMixin):
|
|||||||
|
|
||||||
class TenantBulkEditForm(forms.Form, BootstrapMixin):
|
class TenantBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
group = forms.ModelChoiceField(queryset=TenantGroup.objects.all(), required=False)
|
group = forms.TypedChoiceField(choices=bulkedit_tenantgroup_choices, coerce=int, required=False, label='Group')
|
||||||
|
|
||||||
|
|
||||||
def tenant_group_choices():
|
def tenant_group_choices():
|
||||||
|
21
netbox/tenancy/migrations/0002_tenant_group_optional.py
Normal file
21
netbox/tenancy/migrations/0002_tenant_group_optional.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.8 on 2016-08-02 19:54
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('tenancy', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='tenant',
|
||||||
|
name='group',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='tenants', to='tenancy.TenantGroup'),
|
||||||
|
),
|
||||||
|
]
|
@ -28,7 +28,7 @@ class Tenant(CreatedUpdatedModel):
|
|||||||
"""
|
"""
|
||||||
name = models.CharField(max_length=30, unique=True)
|
name = models.CharField(max_length=30, unique=True)
|
||||||
slug = models.SlugField(unique=True)
|
slug = models.SlugField(unique=True)
|
||||||
group = models.ForeignKey('TenantGroup', related_name='tenants', on_delete=models.PROTECT)
|
group = models.ForeignKey('TenantGroup', related_name='tenants', blank=True, null=True, on_delete=models.SET_NULL)
|
||||||
description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)")
|
description = models.CharField(max_length=100, blank=True, help_text="Long-form name (optional)")
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
|
|
||||||
|
@ -2,6 +2,9 @@ from django.contrib.auth.mixins import PermissionRequiredMixin
|
|||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
|
from circuits.models import Circuit
|
||||||
|
from dcim.models import Site, Rack, Device
|
||||||
|
from ipam.models import IPAddress, Prefix, VLAN, VRF
|
||||||
from utilities.views import (
|
from utilities.views import (
|
||||||
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
BulkDeleteView, BulkEditView, BulkImportView, ObjectDeleteView, ObjectEditView, ObjectListView,
|
||||||
)
|
)
|
||||||
@ -50,19 +53,21 @@ class TenantListView(ObjectListView):
|
|||||||
|
|
||||||
def tenant(request, slug):
|
def tenant(request, slug):
|
||||||
|
|
||||||
tenant = get_object_or_404(Tenant.objects.annotate(
|
tenant = get_object_or_404(Tenant, slug=slug)
|
||||||
site_count=Count('sites', distinct=True),
|
stats = {
|
||||||
rack_count=Count('racks', distinct=True),
|
'site_count': Site.objects.filter(tenant=tenant).count(),
|
||||||
device_count=Count('devices', distinct=True),
|
'rack_count': Rack.objects.filter(tenant=tenant).count(),
|
||||||
vrf_count=Count('vrfs', distinct=True),
|
'device_count': Device.objects.filter(tenant=tenant).count(),
|
||||||
prefix_count=Count('prefixes', distinct=True),
|
'vrf_count': VRF.objects.filter(tenant=tenant).count(),
|
||||||
ipaddress_count=Count('ip_addresses', distinct=True),
|
'prefix_count': Prefix.objects.filter(tenant=tenant).count(),
|
||||||
vlan_count=Count('vlans', distinct=True),
|
'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(),
|
||||||
circuit_count=Count('circuits', distinct=True),
|
'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
|
||||||
), slug=slug)
|
'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
|
||||||
|
}
|
||||||
|
|
||||||
return render(request, 'tenancy/tenant.html', {
|
return render(request, 'tenancy/tenant.html', {
|
||||||
'tenant': tenant,
|
'tenant': tenant,
|
||||||
|
'stats': stats,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@ -99,9 +104,10 @@ class TenantBulkEditView(PermissionRequiredMixin, BulkEditView):
|
|||||||
def update_objects(self, pk_list, form):
|
def update_objects(self, pk_list, form):
|
||||||
|
|
||||||
fields_to_update = {}
|
fields_to_update = {}
|
||||||
for field in ['group']:
|
if form.cleaned_data['group'] == 0:
|
||||||
if form.cleaned_data[field]:
|
fields_to_update['group'] = None
|
||||||
fields_to_update[field] = form.cleaned_data[field]
|
elif form.cleaned_data['group']:
|
||||||
|
fields_to_update['group'] = form.cleaned_data['group']
|
||||||
|
|
||||||
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
return self.cls.objects.filter(pk__in=pk_list).update(**fields_to_update)
|
||||||
|
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import csv
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
@ -118,7 +119,8 @@ class Livesearch(forms.TextInput):
|
|||||||
|
|
||||||
class CSVDataField(forms.CharField):
|
class CSVDataField(forms.CharField):
|
||||||
"""
|
"""
|
||||||
A field for comma-separated values (CSV)
|
A field for comma-separated values (CSV). Values containing commas should be encased within double quotes. Example:
|
||||||
|
'"New York, NY",new-york-ny,Other stuff' => ['New York, NY', 'new-york-ny', 'Other stuff']
|
||||||
"""
|
"""
|
||||||
csv_form = None
|
csv_form = None
|
||||||
|
|
||||||
@ -136,16 +138,16 @@ class CSVDataField(forms.CharField):
|
|||||||
def to_python(self, value):
|
def to_python(self, value):
|
||||||
# Return a list of dictionaries, each representing an individual record
|
# Return a list of dictionaries, each representing an individual record
|
||||||
records = []
|
records = []
|
||||||
for i, row in enumerate(value.split('\n'), start=1):
|
reader = csv.reader(value.splitlines())
|
||||||
if row.strip():
|
for i, row in enumerate(reader, start=1):
|
||||||
values = row.strip().split(',')
|
if row:
|
||||||
if len(values) < len(self.columns):
|
if len(row) < len(self.columns):
|
||||||
raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})"
|
raise forms.ValidationError("Line {}: Field(s) missing (found {}; expected {})"
|
||||||
.format(i, len(values), len(self.columns)))
|
.format(i, len(row), len(self.columns)))
|
||||||
elif len(values) > len(self.columns):
|
elif len(row) > len(self.columns):
|
||||||
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
|
raise forms.ValidationError("Line {}: Too many fields (found {}; expected {})"
|
||||||
.format(i, len(values), len(self.columns)))
|
.format(i, len(row), len(self.columns)))
|
||||||
record = dict(zip(self.columns, values))
|
record = dict(zip(self.columns, row))
|
||||||
records.append(record)
|
records.append(record)
|
||||||
return records
|
return records
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user