Merge branch 'develop' into feature

This commit is contained in:
jeremystretch 2021-05-07 10:27:23 -04:00
commit c777daf6e6
26 changed files with 103 additions and 111 deletions

View File

@ -17,7 +17,7 @@ body:
What version of NetBox are you currently running? (If you don't have access to the most What version of NetBox are you currently running? (If you don't have access to the most
recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/) recent NetBox release, consider testing on our [demo instance](https://demo.netbox.dev/)
before opening a bug report to see if your issue has already been addressed.) before opening a bug report to see if your issue has already been addressed.)
placeholder: v2.11.2 placeholder: v2.11.3
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes: attributes:
label: NetBox version label: NetBox version
description: What version of NetBox are you currently running? description: What version of NetBox are you currently running?
placeholder: v2.10.4 placeholder: v2.11.3
validations: validations:
required: true required: true
- type: dropdown - type: dropdown

View File

@ -17,8 +17,8 @@ jobs:
necessary. necessary.
close-pr-message: > close-pr-message: >
This PR has been automatically closed due to lack of activity. This PR has been automatically closed due to lack of activity.
days-before-stale: 45 days-before-stale: 60
days-before-close: 15 days-before-close: 30
exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone' exempt-issue-labels: 'status: accepted,status: blocked,status: needs milestone'
operations-per-run: 100 operations-per-run: 100
remove-stale-when-updated: false remove-stale-when-updated: false

View File

@ -1,4 +1,6 @@
![NetBox](docs/netbox_logo.svg "NetBox logo") <div align="center">
<img src="https://raw.githubusercontent.com/netbox-community/netbox/develop/docs/netbox_logo.svg" width="400" alt="NetBox logo" />
</div>
NetBox is an IP address management (IPAM) and data center infrastructure NetBox is an IP address management (IPAM) and data center infrastructure
management (DCIM) tool. Initially conceived by the network engineering team at management (DCIM) tool. Initially conceived by the network engineering team at
@ -12,43 +14,34 @@ complete list of requirements, see `requirements.txt`. The code is available [on
The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev. The complete documentation for NetBox can be found at [Read the Docs](https://netbox.readthedocs.io/en/stable/). A public demo instance is available at https://demo.netbox.dev.
| | status |
|-------------|------------|
| **master** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) |
| **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) |
<div align="center">
<h4>Thank you to our sponsors!</h4>
[![NS1](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/ns1.png)](https://ns1.com/)
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
[![Stellar Technologies](https://raw.githubusercontent.com/wiki/netbox-community/netbox/images/sponsors/stellar.png)](https://stellar.tech/)
</div>
### Discussion ### Discussion
* [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions * [GitHub Discussions](https://github.com/netbox-community/netbox/discussions) - Discussion forum hosted by GitHub; ideal for Q&A and other structured discussions
* [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out * [Slack](https://slack.netbox.dev/) - Real-time chat hosted by the NetDev Community; best for unstructured discussion or just hanging out
* [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions * [Google Group](https://groups.google.com/g/netbox-discuss) - Legacy mailing list; slowly being replaced by GitHub discussions
### Build Status ### Installation
| | status |
| ----------- | ------------------------------------------------------------------------------------------------- |
| **master** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=master) |
| **develop** | ![Build status](https://github.com/netbox-community/netbox/workflows/CI/badge.svg?branch=develop) |
### Screenshots
![Screenshot of Main Page](docs/media/home-light.png "Main Page")
---
![Screenshot of Rack Elevation](docs/media/rack-dark.png "Rack Elevation")
---
![Screenshot of Prefix Hierarchy](docs/media/prefixes-light.png "Prefix Hierarchy")
---
![Screenshot of Cable Tracing](docs/media/cable-dark.png "Cable Tracing")
## Installation
Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for Please see [the documentation](https://netbox.readthedocs.io/en/stable/) for
instructions on installing NetBox. To upgrade NetBox, please download the instructions on installing NetBox. To upgrade NetBox, please download the
[latest release](https://github.com/netbox-community/netbox/releases) and [latest release](https://github.com/netbox-community/netbox/releases) and
run `upgrade.sh`. run `upgrade.sh`.
## Providing Feedback ### Providing Feedback
The best platform for general feedback, assistance, and other discussion is our The best platform for general feedback, assistance, and other discussion is our
[GitHub discussions](https://github.com/netbox-community/netbox/discussions). [GitHub discussions](https://github.com/netbox-community/netbox/discussions).
@ -58,7 +51,17 @@ the [appropriate template](https://github.com/netbox-community/netbox/issues/new
If you are interested in contributing to the development of NetBox, please read If you are interested in contributing to the development of NetBox, please read
our [contributing guide](CONTRIBUTING.md) prior to beginning any work. our [contributing guide](CONTRIBUTING.md) prior to beginning any work.
## Related projects ### Screenshots
![Screenshot of Main Page](docs/media/home-light.png "Main Page")
![Screenshot of Rack Elevation](docs/media/rack-dark.png "Rack Elevation")
![Screenshot of Prefix Hierarchy](docs/media/prefixes-light.png "Prefix Hierarchy")
![Screenshot of Cable Tracing](docs/media/cable-dark.png "Cable Tracing")
### Related projects
Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions) Please see [our wiki](https://github.com/netbox-community/netbox/wiki/Community-Contributions)
for a list of relevant community projects. for a list of relevant community projects.

View File

@ -70,7 +70,11 @@ Ensure that continuous integration testing on the `develop` branch is completing
### Update Version and Changelog ### Update Version and Changelog
Update the `VERSION` constant in `settings.py` to the new release version and annotate the current data in the release notes for the new version. Commit these changes to the `develop` branch. * Update the `VERSION` constant in `settings.py` to the new release version.
* Update the example version numbers in the feature request and bug report templates under `.github/ISSUE_TEMPLATES/`.
* Replace the "FUTURE" placeholder in the release notes with the current date.
Commit these changes to the `develop` branch.
### Submit a Pull Request ### Submit a Pull Request

View File

@ -1,21 +1,29 @@
# NetBox v2.11 # NetBox v2.11
## v2.11.3 (FUTURE) ## v2.11.3 (2021-05-07)
### Enhancements ### Enhancements
* [#6197](https://github.com/netbox-community/netbox/issues/6197) - Introduced `SESSION_COOKIE_NAME` config parameter * [#6197](https://github.com/netbox-community/netbox/issues/6197) - Introduced `SESSION_COOKIE_NAME` config parameter
* [#6318](https://github.com/netbox-community/netbox/issues/6318) - Add OM5 MMF cable type * [#6318](https://github.com/netbox-community/netbox/issues/6318) - Add OM5 MMF cable type
* [#6351](https://github.com/netbox-community/netbox/issues/6351) - Add aggregates count to tenant view
* [#6359](https://github.com/netbox-community/netbox/issues/6359) - Enable custom links for organizational and nested group models
### Bug Fixes ### Bug Fixes
* [#6240](https://github.com/netbox-community/netbox/issues/6240) - Fix display of available VLAN ranges under VLAN group view * [#6240](https://github.com/netbox-community/netbox/issues/6240) - Fix display of available VLAN ranges under VLAN group view
* [#6308](https://github.com/netbox-community/netbox/issues/6308) - Fix linking of available VLANs in VLAN group view * [#6308](https://github.com/netbox-community/netbox/issues/6308) - Fix linking of available VLANs in VLAN group view
* [#6309](https://github.com/netbox-community/netbox/issues/6309) - Restrict parent VM interface assignment to the parent VM * [#6309](https://github.com/netbox-community/netbox/issues/6309) - Restrict parent VM interface assignment to the parent VM
* [#6312](https://github.com/netbox-community/netbox/issues/6312) - Interface device filter should return all virtual chassis interfaces only if device is master
* [#6313](https://github.com/netbox-community/netbox/issues/6313) - Fix device type instance count under manufacturer view * [#6313](https://github.com/netbox-community/netbox/issues/6313) - Fix device type instance count under manufacturer view
* [#6321](https://github.com/netbox-community/netbox/issues/6321) - Restore "add an IP" button under prefix IPs view * [#6321](https://github.com/netbox-community/netbox/issues/6321) - Restore "add an IP" button under prefix IPs view
* [#6333](https://github.com/netbox-community/netbox/issues/6333) - Fix filtering of circuit terminations by primary key * [#6333](https://github.com/netbox-community/netbox/issues/6333) - Fix filtering of circuit terminations by primary key
* [#6339](https://github.com/netbox-community/netbox/issues/6339) - Improve ordering of interfaces when viewing virtual chassis master * [#6339](https://github.com/netbox-community/netbox/issues/6339) - Improve ordering of interfaces when viewing virtual chassis master
* [#6350](https://github.com/netbox-community/netbox/issues/6350) - Include first & last IP addresses when allocating available IPv6 addresses via the REST API
* [#6355](https://github.com/netbox-community/netbox/issues/6355) - Fix caching error when swapping A/Z circuit terminations
* [#6357](https://github.com/netbox-community/netbox/issues/6357) - Fix ProviderNetwork nested API serializer
* [#6363](https://github.com/netbox-community/netbox/issues/6363) - Correct pre-population of cluster group when creating a cluster
* [#6369](https://github.com/netbox-community/netbox/issues/6369) - Fix interface assignment for VLANs in non-scoped groups
--- ---

View File

@ -20,7 +20,7 @@ class NestedProviderNetworkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail') url = serializers.HyperlinkedIdentityField(view_name='circuits-api:providernetwork-detail')
class Meta: class Meta:
model = Provider model = ProviderNetwork
fields = ['id', 'url', 'display', 'name'] fields = ['id', 'url', 'display', 'name']

View File

@ -149,7 +149,7 @@ class ProviderNetwork(PrimaryModel):
) )
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class CircuitType(OrganizationalModel): class CircuitType(OrganizationalModel):
""" """
Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named

View File

@ -1,9 +1,8 @@
from django.db.models.signals import post_delete, post_save from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver from django.dispatch import receiver
from django.utils import timezone
from dcim.signals import rebuild_paths from dcim.signals import rebuild_paths
from .models import Circuit, CircuitTermination from .models import CircuitTermination
@receiver(post_save, sender=CircuitTermination) @receiver(post_save, sender=CircuitTermination)
@ -11,11 +10,9 @@ def update_circuit(instance, **kwargs):
""" """
When a CircuitTermination has been modified, update its parent Circuit. When a CircuitTermination has been modified, update its parent Circuit.
""" """
fields = { termination_name = f'termination_{instance.term_side.lower()}'
'last_updated': timezone.now(), setattr(instance.circuit, termination_name, instance)
f'termination_{instance.term_side.lower()}': instance.pk, instance.circuit.save()
}
Circuit.objects.filter(pk=instance.circuit_id).update(**fields)
@receiver((post_save, post_delete), sender=CircuitTermination) @receiver((post_save, post_delete), sender=CircuitTermination)

View File

@ -211,27 +211,6 @@ class CircuitListView(generic.ObjectListView):
class CircuitView(generic.ObjectView): class CircuitView(generic.ObjectView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
def get_extra_context(self, request, instance):
# A-side termination
termination_a = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region'
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_A
).first()
# Z-side termination
termination_z = CircuitTermination.objects.restrict(request.user, 'view').prefetch_related(
'site__region'
).filter(
circuit=instance, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
return {
'termination_a': termination_a,
'termination_z': termination_z,
}
class CircuitEditView(generic.ObjectEditView): class CircuitEditView(generic.ObjectEditView):
queryset = Circuit.objects.all() queryset = Circuit.objects.all()
@ -296,16 +275,11 @@ class CircuitSwapTerminations(generic.ObjectEditView):
if form.is_valid(): if form.is_valid():
termination_a = CircuitTermination.objects.filter( termination_a = CircuitTermination.objects.filter(pk=circuit.termination_a_id).first()
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_A termination_z = CircuitTermination.objects.filter(pk=circuit.termination_z_id).first()
).first()
termination_z = CircuitTermination.objects.filter(
circuit=circuit, term_side=CircuitTerminationSideChoices.SIDE_Z
).first()
if termination_a and termination_z: if termination_a and termination_z:
# Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint # Use a placeholder to avoid an IntegrityError on the (circuit, term_side) unique constraint
print('swapping')
with transaction.atomic(): with transaction.atomic():
termination_a.term_side = '_' termination_a.term_side = '_'
termination_a.save() termination_a.save()
@ -316,11 +290,20 @@ class CircuitSwapTerminations(generic.ObjectEditView):
elif termination_a: elif termination_a:
termination_a.term_side = 'Z' termination_a.term_side = 'Z'
termination_a.save() termination_a.save()
circuit.refresh_from_db()
circuit.termination_a = None
circuit.save()
else: else:
termination_z.term_side = 'A' termination_z.term_side = 'A'
termination_z.save() termination_z.save()
circuit.refresh_from_db()
circuit.termination_z = None
circuit.save()
messages.success(request, "Swapped terminations for circuit {}.".format(circuit)) print(f'term A: {circuit.termination_a}')
print(f'term Z: {circuit.termination_z}')
messages.success(request, f"Swapped terminations for circuit {circuit}.")
return redirect('circuits:circuit', pk=circuit.pk) return redirect('circuits:circuit', pk=circuit.pk)
return render(request, 'circuits/circuit_terminations_swap.html', { return render(request, 'circuits/circuit_terminations_swap.html', {

View File

@ -2153,7 +2153,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
ip_choices = [(None, '---------')] ip_choices = [(None, '---------')]
# Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member # Gather PKs of all interfaces belonging to this Device or a peer VirtualChassis member
interface_ids = self.instance.vc_interfaces().values_list('pk', flat=True) interface_ids = self.instance.vc_interfaces(if_master=False).values_list('pk', flat=True)
# Collect interface IPs # Collect interface IPs
interface_ips = IPAddress.objects.filter( interface_ips = IPAddress.objects.filter(

View File

@ -36,7 +36,7 @@ __all__ = (
# Device Types # Device Types
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Manufacturer(OrganizationalModel): class Manufacturer(OrganizationalModel):
""" """
A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell. A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@ -333,7 +333,7 @@ class DeviceType(PrimaryModel):
# Devices # Devices
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class DeviceRole(OrganizationalModel): class DeviceRole(OrganizationalModel):
""" """
Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
@ -384,7 +384,7 @@ class DeviceRole(OrganizationalModel):
) )
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Platform(OrganizationalModel): class Platform(OrganizationalModel):
""" """
Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos". Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
@ -718,7 +718,7 @@ class Device(PrimaryModel, ConfigContextModel):
pass pass
# Validate primary IP addresses # Validate primary IP addresses
vc_interfaces = self.vc_interfaces() vc_interfaces = self.vc_interfaces(if_master=False)
if self.primary_ip4: if self.primary_ip4:
if self.primary_ip4.family != 4: if self.primary_ip4.family != 4:
raise ValidationError({ raise ValidationError({
@ -847,9 +847,7 @@ class Device(PrimaryModel, ConfigContextModel):
@property @property
def interfaces_count(self): def interfaces_count(self):
if self.virtual_chassis and self.virtual_chassis.master == self: return self.vc_interfaces().count()
return self.vc_interfaces().count()
return self.interfaces.count()
def get_vc_master(self): def get_vc_master(self):
""" """
@ -857,7 +855,7 @@ class Device(PrimaryModel, ConfigContextModel):
""" """
return self.virtual_chassis.master if self.virtual_chassis else None return self.virtual_chassis.master if self.virtual_chassis else None
def vc_interfaces(self, if_master=False): def vc_interfaces(self, if_master=True):
""" """
Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another Return a QuerySet matching all Interfaces assigned to this Device or, if this Device is a VC master, to another
Device belonging to the same VirtualChassis. Device belonging to the same VirtualChassis.
@ -865,7 +863,7 @@ class Device(PrimaryModel, ConfigContextModel):
:param if_master: If True, return VC member interfaces only if this Device is the VC master. :param if_master: If True, return VC member interfaces only if this Device is the VC master.
""" """
filter = Q(device=self) filter = Q(device=self)
if self.virtual_chassis and (not if_master or self.virtual_chassis.master == self): if self.virtual_chassis and (self.virtual_chassis.master == self or not if_master):
filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False) filter |= Q(device__virtual_chassis=self.virtual_chassis, mgmt_only=False)
return Interface.objects.filter(filter) return Interface.objects.filter(filter)

View File

@ -35,7 +35,7 @@ __all__ = (
# Racks # Racks
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RackRole(OrganizationalModel): class RackRole(OrganizationalModel):
""" """
Racks can be organized by functional role, similar to Devices. Racks can be organized by functional role, similar to Devices.

View File

@ -26,7 +26,7 @@ __all__ = (
# Regions # Regions
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Region(NestedGroupModel): class Region(NestedGroupModel):
""" """
A region represents a geographic collection of sites. For example, you might create regions representing countries, A region represents a geographic collection of sites. For example, you might create regions representing countries,
@ -78,7 +78,7 @@ class Region(NestedGroupModel):
# Site groups # Site groups
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class SiteGroup(NestedGroupModel): class SiteGroup(NestedGroupModel):
""" """
A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
@ -285,7 +285,7 @@ class Site(PrimaryModel):
# Locations # Locations
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Location(NestedGroupModel): class Location(NestedGroupModel):
""" """
A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a

View File

@ -1407,7 +1407,7 @@ class DeviceInterfacesView(generic.ObjectView):
template_name = 'dcim/device/interfaces.html' template_name = 'dcim/device/interfaces.html'
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related( interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)), Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)), Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
'lag', 'cable', '_path__destination', 'tags', 'lag', 'cable', '_path__destination', 'tags',
@ -1529,7 +1529,7 @@ class DeviceLLDPNeighborsView(generic.ObjectView):
template_name = 'dcim/device/lldp_neighbors.html' template_name = 'dcim/device/lldp_neighbors.html'
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
interfaces = instance.vc_interfaces(if_master=True).restrict(request.user, 'view').prefetch_related( interfaces = instance.vc_interfaces().restrict(request.user, 'view').prefetch_related(
'_path__destination' '_path__destination'
).exclude( ).exclude(
type__in=NONCONNECTABLE_IFACE_TYPES type__in=NONCONNECTABLE_IFACE_TYPES

View File

@ -29,7 +29,7 @@ __all__ = (
) )
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RIR(OrganizationalModel): class RIR(OrganizationalModel):
""" """
A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
@ -184,7 +184,7 @@ class Aggregate(PrimaryModel):
return int(float(child_prefixes.size) / self.prefix.size * 100) return int(float(child_prefixes.size) / self.prefix.size * 100)
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Role(OrganizationalModel): class Role(OrganizationalModel):
""" """
A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
@ -426,19 +426,11 @@ class Prefix(PrimaryModel):
child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()]) child_ips = netaddr.IPSet([ip.address.ip for ip in self.get_child_ips()])
available_ips = prefix - child_ips available_ips = prefix - child_ips
# All IP addresses within a pool are considered usable # IPv6, pool, or IPv4 /31 sets are fully usable
if self.is_pool: if self.family == 6 or self.is_pool or self.prefix.prefixlen == 31:
return available_ips return available_ips
# All IP addresses within a point-to-point prefix (IPv4 /31 or IPv6 /127) are considered usable # For "normal" IPv4 prefixes, omit first and last addresses
if (
self.prefix.version == 4 and self.prefix.prefixlen == 31 # RFC 3021
) or (
self.prefix.version == 6 and self.prefix.prefixlen == 127 # RFC 6164
):
return available_ips
# Omit first and last IP address from the available set
available_ips -= netaddr.IPSet([ available_ips -= netaddr.IPSet([
netaddr.IPAddress(self.prefix.first), netaddr.IPAddress(self.prefix.first),
netaddr.IPAddress(self.prefix.last), netaddr.IPAddress(self.prefix.last),

View File

@ -21,7 +21,7 @@ __all__ = (
) )
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VLANGroup(OrganizationalModel): class VLANGroup(OrganizationalModel):
""" """
A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique. A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.

View File

@ -64,6 +64,7 @@ class VLANQuerySet(RestrictedQuerySet):
return self.filter( return self.filter(
Q(group__in=VLANGroup.objects.filter(q)) | Q(group__in=VLANGroup.objects.filter(q)) |
Q(site=device.site) | Q(site=device.site) |
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs
) )
@ -104,6 +105,7 @@ class VLANQuerySet(RestrictedQuerySet):
# Return all applicable VLANs # Return all applicable VLANs
q = ( q = (
Q(group__in=vlan_groups) | Q(group__in=vlan_groups) |
Q(group__scope_id__isnull=True, site__isnull=True) | # Global group VLANs
Q(group__isnull=True, site__isnull=True) # Global VLANs Q(group__isnull=True, site__isnull=True) # Global VLANs
) )
if vm.cluster.site: if vm.cluster.site:

View File

@ -233,7 +233,7 @@ class SessionKey(BigIDModel):
return session_key return session_key
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class SecretRole(OrganizationalModel): class SecretRole(OrganizationalModel):
""" """
A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles A SecretRole represents an arbitrary functional classification of Secrets. For example, a user might define roles

View File

@ -82,8 +82,8 @@
{% plugin_left_page object %} {% plugin_left_page object %}
</div> </div>
<div class="col col-md-6"> <div class="col col-md-6">
{% include 'circuits/inc/circuit_termination.html' with termination=termination_a side='A' %} {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_a side='A' %}
{% include 'circuits/inc/circuit_termination.html' with termination=termination_z side='Z' %} {% include 'circuits/inc/circuit_termination.html' with termination=object.termination_z side='Z' %}
{% plugin_right_page object %} {% plugin_right_page object %}
</div> </div>
</div> </div>

View File

@ -78,6 +78,10 @@
<h2><a href="{% url 'ipam:vrf_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vrf_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2> <h2><a href="{% url 'ipam:vrf_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.vrf_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.vrf_count }}</a></h2>
<p>VRFs</p> <p>VRFs</p>
</div> </div>
<div class="col col-md-4 text-center">
<h2><a href="{% url 'ipam:aggregate_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.aggregate_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.aggregate_count }}</a></h2>
<p>Aggregates</p>
</div>
<div class="col col-md-4 text-center"> <div class="col col-md-4 text-center">
<h2><a href="{% url 'ipam:prefix_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.prefix_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2> <h2><a href="{% url 'ipam:prefix_list' %}?tenant_id={{ object.pk }}" class="stat-btn btn {% if stats.prefix_count %}btn-primary{% else %}btn-outline-dark{% endif %} btn-lg">{{ stats.prefix_count }}</a></h2>
<p>Prefixes</p> <p>Prefixes</p>

View File

@ -51,7 +51,7 @@
</div> </div>
{% if perms.virtualization.add_cluster %} {% if perms.virtualization.add_cluster %}
<div class="card-footer text-end noprint"> <div class="card-footer text-end noprint">
<a href="{% url 'virtualization:cluster_add' %}?type={{ object.pk }}" class="btn btn-sm btn-primary"> <a href="{% url 'virtualization:cluster_add' %}?group={{ object.pk }}" class="btn btn-sm btn-primary">
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Cluster <span class="mdi mdi-plus-thick" aria-hidden="true"></span> Add Cluster
</a> </a>
</div> </div>

View File

@ -14,7 +14,7 @@ __all__ = (
) )
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class TenantGroup(NestedGroupModel): class TenantGroup(NestedGroupModel):
""" """
An arbitrary collection of Tenants. An arbitrary collection of Tenants.

View File

@ -1,6 +1,6 @@
from circuits.models import Circuit from circuits.models import Circuit
from dcim.models import Site, Rack, Device, RackReservation from dcim.models import Site, Rack, Device, RackReservation
from ipam.models import IPAddress, Prefix, VLAN, VRF from ipam.models import Aggregate, IPAddress, Prefix, VLAN, VRF
from netbox.views import generic from netbox.views import generic
from utilities.tables import paginate_table from utilities.tables import paginate_table
from virtualization.models import VirtualMachine, Cluster from virtualization.models import VirtualMachine, Cluster
@ -101,6 +101,7 @@ class TenantView(generic.ObjectView):
'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'device_count': Device.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'vrf_count': VRF.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'vrf_count': VRF.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'prefix_count': Prefix.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'aggregate_count': Aggregate.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'ipaddress_count': IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'ipaddress_count': IPAddress.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'vlan_count': VLAN.objects.restrict(request.user, 'view').filter(tenant=instance).count(),
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(), 'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(tenant=instance).count(),

View File

@ -30,7 +30,7 @@ __all__ = (
# Cluster types # Cluster types
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ClusterType(OrganizationalModel): class ClusterType(OrganizationalModel):
""" """
A type of Cluster. A type of Cluster.
@ -73,7 +73,7 @@ class ClusterType(OrganizationalModel):
# Cluster groups # Cluster groups
# #
@extras_features('custom_fields', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class ClusterGroup(OrganizationalModel): class ClusterGroup(OrganizationalModel):
""" """
An organizational group of Clusters. An organizational group of Clusters.

View File

@ -1,5 +1,5 @@
Django==3.2 Django==3.2.2
django-cacheops==5.1 django-cacheops==6.0
django-cors-headers==3.7.0 django-cors-headers==3.7.0
django-debug-toolbar==3.2.1 django-debug-toolbar==3.2.1
django-filter==2.4.0 django-filter==2.4.0