mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-18 13:06:30 -06:00
commit
7fc60cd667
1975
CHANGELOG.md
1975
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -16,11 +16,11 @@ For real-time discussion, you can join the #netbox Slack channel on [NetworkToCo
|
|||||||
|
|
||||||
## Reporting Bugs
|
## Reporting Bugs
|
||||||
|
|
||||||
* First, ensure that you've installed the [latest stable version](https://github.com/digitalocean/netbox/releases)
|
* First, ensure that you've installed the [latest stable version](https://github.com/netbox-community/netbox/releases)
|
||||||
of NetBox. If you're running an older version, it's possible that the bug has
|
of NetBox. If you're running an older version, it's possible that the bug has
|
||||||
already been fixed.
|
already been fixed.
|
||||||
|
|
||||||
* Next, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
* Next, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues)
|
||||||
to see if the bug you've found has already been reported. If you think you may
|
to see if the bug you've found has already been reported. If you think you may
|
||||||
be experiencing a reported issue that hasn't already been resolved, please
|
be experiencing a reported issue that hasn't already been resolved, please
|
||||||
click "add a reaction" in the top right corner of the issue and add a thumbs
|
click "add a reaction" in the top right corner of the issue and add a thumbs
|
||||||
@ -51,7 +51,7 @@ your issue.
|
|||||||
|
|
||||||
## Feature Requests
|
## Feature Requests
|
||||||
|
|
||||||
* First, check the GitHub [issues list](https://github.com/digitalocean/netbox/issues)
|
* First, check the GitHub [issues list](https://github.com/netbox-community/netbox/issues)
|
||||||
to see if the feature you're requesting is already listed. (Be sure to search
|
to see if the feature you're requesting is already listed. (Be sure to search
|
||||||
closed issues as well, since some feature requests have been rejected.) If the
|
closed issues as well, since some feature requests have been rejected.) If the
|
||||||
feature you'd like to see has already been requested and is open, click "add a
|
feature you'd like to see has already been requested and is open, click "add a
|
||||||
|
1
NOTICE
Normal file
1
NOTICE
Normal file
@ -0,0 +1 @@
|
|||||||
|
Copyrighted and licensed under Apache License 2.0 by DigitalOcean, LLC.
|
@ -7,7 +7,7 @@ to address the needs of network and infrastructure engineers.
|
|||||||
|
|
||||||
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
|
NetBox runs as a web application atop the [Django](https://www.djangoproject.com/)
|
||||||
Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a
|
Python framework with a [PostgreSQL](http://www.postgresql.org/) database. For a
|
||||||
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/digitalocean/netbox).
|
complete list of requirements, see `requirements.txt`. The code is available [on GitHub](https://github.com/netbox-community/netbox).
|
||||||
|
|
||||||
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
|
The complete documentation for NetBox can be found at [Read the Docs](http://netbox.readthedocs.io/en/stable/).
|
||||||
|
|
||||||
@ -32,7 +32,7 @@ or join us in the #netbox Slack channel on [NetworkToCode](https://networktocode
|
|||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
|
Please see [the documentation](http://netbox.readthedocs.io/en/stable/) for
|
||||||
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/digitalocean/netbox/releases)
|
instructions on installing NetBox. To upgrade NetBox, please download the [latest release](https://github.com/netbox-community/netbox/releases)
|
||||||
and run `upgrade.sh`.
|
and run `upgrade.sh`.
|
||||||
|
|
||||||
## Alternative Installations
|
## Alternative Installations
|
||||||
|
@ -95,7 +95,7 @@ Pass-through ports can also be used to model "bump in the wire" devices, such as
|
|||||||
|
|
||||||
### Device Bays
|
### Device Bays
|
||||||
|
|
||||||
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations, but they are included in the "Non-Racked Devices" list within the rack view.
|
Device bays represent the ability of a device to house child devices. For example, you might install four blade servers into a 2U chassis. The chassis would appear in the rack elevation as a 2U device with four device bays. Each server within it would be defined as a 0U device installed in one of the device bays. Child devices do not appear within rack elevations or the "Non-Racked Devices" list within the rack view.
|
||||||
|
|
||||||
Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices.
|
Child devices are first-class Devices in their own right: that is, fully independent managed entities which don't share any control plane with the parent. Just like normal devices, child devices have their own platform (OS), role, tags, and interfaces. You cannot create a LAG between interfaces in different child devices.
|
||||||
|
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
# NetBox Development
|
# NetBox Development
|
||||||
|
|
||||||
NetBox is maintained as a [GitHub project](https://github.com/digitalocean/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
|
NetBox is maintained as a [GitHub project](https://github.com/netbox-community/netbox) under the Apache 2 license. Users are encouraged to submit GitHub issues for feature requests and bug reports, however we are very selective about pull requests. Please see the `CONTRIBUTING` guide for more direction on contributing to NetBox.
|
||||||
|
|
||||||
## Communication
|
## Communication
|
||||||
|
|
||||||
Communication among developers should always occur via public channels:
|
Communication among developers should always occur via public channels:
|
||||||
|
|
||||||
* [GitHub issues](https://github.com/digitalocean/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
|
* [GitHub issues](https://github.com/netbox-community/netbox/issues) - All feature requests, bug reports, and other substantial changes to the code base **must** be documented in an issue.
|
||||||
* [The mailing list](https://groups.google.com/forum/#!forum/netbox-discuss) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
* [The mailing list](https://groups.google.com/forum/#!forum/netbox-discuss) - The preferred forum for general discussion and support issues. Ideal for shaping a feature request prior to submitting an issue.
|
||||||
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
* [#netbox on NetworkToCode](http://slack.networktocode.com/) - Good for quick chats. Avoid any discussion that might need to be referenced later on, as the chat history is not retained long.
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ Once CI has completed on the PR, merge it.
|
|||||||
|
|
||||||
## Create a New Release
|
## Create a New Release
|
||||||
|
|
||||||
Draft a [new release](https://github.com/digitalocean/netbox/releases/new) with the following parameters.
|
Draft a [new release](https://github.com/netbox-community/netbox/releases/new) with the following parameters.
|
||||||
|
|
||||||
* **Tag:** Current version (e.g. `v2.3.4`)
|
* **Tag:** Current version (e.g. `v2.3.4`)
|
||||||
* **Target:** `master`
|
* **Target:** `master`
|
||||||
|
@ -21,10 +21,10 @@ You may opt to install NetBox either from a numbered release or by cloning the m
|
|||||||
|
|
||||||
## Option A: Download a Release
|
## Option A: Download a Release
|
||||||
|
|
||||||
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive and extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
||||||
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||||
# cd /opt/
|
# cd /opt/
|
||||||
# ln -s netbox-X.Y.Z/ netbox
|
# ln -s netbox-X.Y.Z/ netbox
|
||||||
@ -56,7 +56,7 @@ If `git` is not already installed, install it:
|
|||||||
Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
|
Next, clone the **master** branch of the NetBox GitHub repository into the current directory:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# git clone -b master https://github.com/digitalocean/netbox.git .
|
# git clone -b master https://github.com/netbox-community/netbox.git .
|
||||||
Cloning into '.'...
|
Cloning into '.'...
|
||||||
remote: Counting objects: 1994, done.
|
remote: Counting objects: 1994, done.
|
||||||
remote: Compressing objects: 100% (150/150), done.
|
remote: Compressing objects: 100% (150/150), done.
|
||||||
|
@ -4,12 +4,12 @@ As with the initial installation, you can upgrade NetBox by either downloading t
|
|||||||
|
|
||||||
## Option A: Download a Release
|
## Option A: Download a Release
|
||||||
|
|
||||||
Download the [latest stable release](https://github.com/digitalocean/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
Download the [latest stable release](https://github.com/netbox-community/netbox/releases) from GitHub as a tarball or ZIP archive. Extract it to your desired path. In this example, we'll use `/opt/netbox`.
|
||||||
|
|
||||||
Download and extract the latest version:
|
Download and extract the latest version:
|
||||||
|
|
||||||
```no-highlight
|
```no-highlight
|
||||||
# wget https://github.com/digitalocean/netbox/archive/vX.Y.Z.tar.gz
|
# wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
|
||||||
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
# tar -xzf vX.Y.Z.tar.gz -C /opt
|
||||||
# cd /opt/
|
# cd /opt/
|
||||||
# ln -sfn netbox-X.Y.Z/ netbox
|
# ln -sfn netbox-X.Y.Z/ netbox
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
site_name: NetBox
|
site_name: NetBox
|
||||||
theme: readthedocs
|
theme: readthedocs
|
||||||
repo_url: https://github.com/digitalocean/netbox
|
repo_url: https://github.com/netbox-community/netbox
|
||||||
|
|
||||||
pages:
|
pages:
|
||||||
- Introduction: 'index.md'
|
- Introduction: 'index.md'
|
||||||
|
@ -20,15 +20,6 @@ STATUS_LABEL = """
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
class CircuitTerminationColumn(tables.Column):
|
|
||||||
|
|
||||||
def render(self, value):
|
|
||||||
return mark_safe('<a href="{}">{}</a>'.format(
|
|
||||||
value.site.get_absolute_url(),
|
|
||||||
value.site
|
|
||||||
))
|
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Providers
|
# Providers
|
||||||
#
|
#
|
||||||
@ -77,9 +68,13 @@ class CircuitTable(BaseTable):
|
|||||||
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
provider = tables.LinkColumn('circuits:provider', args=[Accessor('provider.slug')])
|
||||||
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
status = tables.TemplateColumn(template_code=STATUS_LABEL, verbose_name='Status')
|
||||||
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
tenant = tables.TemplateColumn(template_code=COL_TENANT)
|
||||||
termination_a = CircuitTerminationColumn(orderable=False, verbose_name='A Side')
|
a_side = tables.Column(
|
||||||
termination_z = CircuitTerminationColumn(orderable=False, verbose_name='Z Side')
|
verbose_name='A Side'
|
||||||
|
)
|
||||||
|
z_side = tables.Column(
|
||||||
|
verbose_name='Z Side'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = Circuit
|
model = Circuit
|
||||||
fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'termination_a', 'termination_z', 'description')
|
fields = ('pk', 'cid', 'status', 'type', 'provider', 'tenant', 'a_side', 'z_side', 'description')
|
||||||
|
@ -1,11 +1,9 @@
|
|||||||
from django.conf import settings
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import permission_required
|
from django.contrib.auth.decorators import permission_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
from django.db.models import Count
|
from django.db.models import Count, OuterRef, Subquery
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils.decorators import method_decorator
|
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
|
|
||||||
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
from extras.models import Graph, GRAPH_TYPE_PROVIDER
|
||||||
@ -135,10 +133,14 @@ class CircuitTypeBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
|||||||
|
|
||||||
class CircuitListView(PermissionRequiredMixin, ObjectListView):
|
class CircuitListView(PermissionRequiredMixin, ObjectListView):
|
||||||
permission_required = 'circuits.view_circuit'
|
permission_required = 'circuits.view_circuit'
|
||||||
|
_terminations = CircuitTermination.objects.filter(circuit=OuterRef('pk'))
|
||||||
queryset = Circuit.objects.select_related(
|
queryset = Circuit.objects.select_related(
|
||||||
'provider', 'type', 'tenant'
|
'provider', 'type', 'tenant'
|
||||||
).prefetch_related(
|
).prefetch_related(
|
||||||
'terminations__site'
|
'terminations__site'
|
||||||
|
).annotate(
|
||||||
|
a_side=Subquery(_terminations.filter(term_side='A').values('site__name')[:1]),
|
||||||
|
z_side=Subquery(_terminations.filter(term_side='Z').values('site__name')[:1]),
|
||||||
)
|
)
|
||||||
filter = filters.CircuitFilter
|
filter = filters.CircuitFilter
|
||||||
filter_form = forms.CircuitFilterForm
|
filter_form = forms.CircuitFilterForm
|
||||||
|
@ -579,6 +579,7 @@ class VirtualChassisViewSet(ModelViewSet):
|
|||||||
member_count=Count('members')
|
member_count=Count('members')
|
||||||
)
|
)
|
||||||
serializer_class = serializers.VirtualChassisSerializer
|
serializer_class = serializers.VirtualChassisSerializer
|
||||||
|
filterset_class = filters.VirtualChassisFilter
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
@ -280,6 +280,7 @@ IFACE_MODE_CHOICES = [
|
|||||||
# Pass-through port types
|
# Pass-through port types
|
||||||
PORT_TYPE_8P8C = 1000
|
PORT_TYPE_8P8C = 1000
|
||||||
PORT_TYPE_110_PUNCH = 1100
|
PORT_TYPE_110_PUNCH = 1100
|
||||||
|
PORT_TYPE_BNC = 1200
|
||||||
PORT_TYPE_ST = 2000
|
PORT_TYPE_ST = 2000
|
||||||
PORT_TYPE_SC = 2100
|
PORT_TYPE_SC = 2100
|
||||||
PORT_TYPE_SC_APC = 2110
|
PORT_TYPE_SC_APC = 2110
|
||||||
@ -296,6 +297,7 @@ PORT_TYPE_CHOICES = [
|
|||||||
[
|
[
|
||||||
[PORT_TYPE_8P8C, '8P8C'],
|
[PORT_TYPE_8P8C, '8P8C'],
|
||||||
[PORT_TYPE_110_PUNCH, '110 Punch'],
|
[PORT_TYPE_110_PUNCH, '110 Punch'],
|
||||||
|
[PORT_TYPE_BNC, 'BNC'],
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
@ -376,6 +378,7 @@ CABLE_TYPE_CAT6A = 1610
|
|||||||
CABLE_TYPE_CAT7 = 1700
|
CABLE_TYPE_CAT7 = 1700
|
||||||
CABLE_TYPE_DAC_ACTIVE = 1800
|
CABLE_TYPE_DAC_ACTIVE = 1800
|
||||||
CABLE_TYPE_DAC_PASSIVE = 1810
|
CABLE_TYPE_DAC_PASSIVE = 1810
|
||||||
|
CABLE_TYPE_COAXIAL = 1900
|
||||||
CABLE_TYPE_MMF = 3000
|
CABLE_TYPE_MMF = 3000
|
||||||
CABLE_TYPE_MMF_OM1 = 3010
|
CABLE_TYPE_MMF_OM1 = 3010
|
||||||
CABLE_TYPE_MMF_OM2 = 3020
|
CABLE_TYPE_MMF_OM2 = 3020
|
||||||
@ -397,6 +400,7 @@ CABLE_TYPE_CHOICES = (
|
|||||||
(CABLE_TYPE_CAT7, 'CAT7'),
|
(CABLE_TYPE_CAT7, 'CAT7'),
|
||||||
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
|
(CABLE_TYPE_DAC_ACTIVE, 'Direct Attach Copper (Active)'),
|
||||||
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
|
(CABLE_TYPE_DAC_PASSIVE, 'Direct Attach Copper (Passive)'),
|
||||||
|
(CABLE_TYPE_COAXIAL, 'Coaxial'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
@ -2,14 +2,15 @@ import django_filters
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from netaddr import EUI
|
|
||||||
from netaddr.core import AddrFormatError
|
|
||||||
|
|
||||||
from extras.filters import CustomFieldFilterSet
|
from extras.filters import CustomFieldFilterSet
|
||||||
from tenancy.filtersets import TenancyFilterSet
|
from tenancy.filtersets import TenancyFilterSet
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.constants import COLOR_CHOICES
|
from utilities.constants import COLOR_CHOICES
|
||||||
from utilities.filters import NameSlugSearchFilterSet, NumericInFilter, TagFilter, TreeNodeMultipleChoiceFilter
|
from utilities.filters import (
|
||||||
|
MultiValueMACAddressFilter, MultiValueNumberFilter, NameSlugSearchFilterSet, NumericInFilter, TagFilter,
|
||||||
|
TreeNodeMultipleChoiceFilter,
|
||||||
|
)
|
||||||
from virtualization.models import Cluster
|
from virtualization.models import Cluster
|
||||||
from .constants import *
|
from .constants import *
|
||||||
from .models import (
|
from .models import (
|
||||||
@ -514,8 +515,8 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
|
|||||||
field_name='device_type__is_full_depth',
|
field_name='device_type__is_full_depth',
|
||||||
label='Is full depth',
|
label='Is full depth',
|
||||||
)
|
)
|
||||||
mac_address = django_filters.CharFilter(
|
mac_address = MultiValueMACAddressFilter(
|
||||||
method='_mac_address',
|
field_name='interfaces__mac_address',
|
||||||
label='MAC address',
|
label='MAC address',
|
||||||
)
|
)
|
||||||
has_primary_ip = django_filters.BooleanFilter(
|
has_primary_ip = django_filters.BooleanFilter(
|
||||||
@ -572,16 +573,6 @@ class DeviceFilter(TenancyFilterSet, CustomFieldFilterSet):
|
|||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
def _mac_address(self, queryset, name, value):
|
|
||||||
value = value.strip()
|
|
||||||
if not value:
|
|
||||||
return queryset
|
|
||||||
try:
|
|
||||||
mac = EUI(value.strip())
|
|
||||||
return queryset.filter(interfaces__mac_address=mac).distinct()
|
|
||||||
except AddrFormatError:
|
|
||||||
return queryset.none()
|
|
||||||
|
|
||||||
def _has_primary_ip(self, queryset, name, value):
|
def _has_primary_ip(self, queryset, name, value):
|
||||||
if value:
|
if value:
|
||||||
return queryset.filter(
|
return queryset.filter(
|
||||||
@ -624,7 +615,7 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
|
|||||||
method='search',
|
method='search',
|
||||||
label='Search',
|
label='Search',
|
||||||
)
|
)
|
||||||
device_id = django_filters.ModelChoiceFilter(
|
device_id = django_filters.ModelMultipleChoiceFilter(
|
||||||
queryset=Device.objects.all(),
|
queryset=Device.objects.all(),
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
@ -705,8 +696,8 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
field_name='name',
|
field_name='name',
|
||||||
label='Device',
|
label='Device',
|
||||||
)
|
)
|
||||||
device_id = django_filters.NumberFilter(
|
device_id = MultiValueNumberFilter(
|
||||||
method='filter_device',
|
method='filter_device_id',
|
||||||
field_name='pk',
|
field_name='pk',
|
||||||
label='Device (ID)',
|
label='Device (ID)',
|
||||||
)
|
)
|
||||||
@ -724,10 +715,7 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
queryset=Interface.objects.all(),
|
queryset=Interface.objects.all(),
|
||||||
label='LAG interface (ID)',
|
label='LAG interface (ID)',
|
||||||
)
|
)
|
||||||
mac_address = django_filters.CharFilter(
|
mac_address = MultiValueMACAddressFilter()
|
||||||
method='_mac_address',
|
|
||||||
label='MAC address',
|
|
||||||
)
|
|
||||||
tag = TagFilter()
|
tag = TagFilter()
|
||||||
vlan_id = django_filters.CharFilter(
|
vlan_id = django_filters.CharFilter(
|
||||||
method='filter_vlan_id',
|
method='filter_vlan_id',
|
||||||
@ -762,6 +750,17 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
except Device.DoesNotExist:
|
except Device.DoesNotExist:
|
||||||
return queryset.none()
|
return queryset.none()
|
||||||
|
|
||||||
|
def filter_device_id(self, queryset, name, id_list):
|
||||||
|
# Include interfaces belonging to peer virtual chassis members
|
||||||
|
vc_interface_ids = []
|
||||||
|
try:
|
||||||
|
devices = Device.objects.filter(pk__in=id_list)
|
||||||
|
for device in devices:
|
||||||
|
vc_interface_ids += device.vc_interfaces.values_list('id', flat=True)
|
||||||
|
return queryset.filter(pk__in=vc_interface_ids)
|
||||||
|
except Device.DoesNotExist:
|
||||||
|
return queryset.none()
|
||||||
|
|
||||||
def filter_vlan_id(self, queryset, name, value):
|
def filter_vlan_id(self, queryset, name, value):
|
||||||
value = value.strip()
|
value = value.strip()
|
||||||
if not value:
|
if not value:
|
||||||
@ -788,16 +787,6 @@ class InterfaceFilter(django_filters.FilterSet):
|
|||||||
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
|
'wireless': queryset.filter(type__in=WIRELESS_IFACE_TYPES),
|
||||||
}.get(value, queryset.none())
|
}.get(value, queryset.none())
|
||||||
|
|
||||||
def _mac_address(self, queryset, name, value):
|
|
||||||
value = value.strip()
|
|
||||||
if not value:
|
|
||||||
return queryset
|
|
||||||
try:
|
|
||||||
mac = EUI(value.strip())
|
|
||||||
return queryset.filter(mac_address=mac)
|
|
||||||
except AddrFormatError:
|
|
||||||
return queryset.none()
|
|
||||||
|
|
||||||
|
|
||||||
class FrontPortFilter(DeviceComponentFilterSet):
|
class FrontPortFilter(DeviceComponentFilterSet):
|
||||||
cabled = django_filters.BooleanFilter(
|
cabled = django_filters.BooleanFilter(
|
||||||
|
@ -7,6 +7,8 @@ from django.contrib.postgres.forms.array import SimpleArrayField
|
|||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from mptt.forms import TreeNodeChoiceField
|
from mptt.forms import TreeNodeChoiceField
|
||||||
|
from netaddr import EUI
|
||||||
|
from netaddr.core import AddrFormatError
|
||||||
from taggit.forms import TagField
|
from taggit.forms import TagField
|
||||||
from timezone_field import TimeZoneFormField
|
from timezone_field import TimeZoneFormField
|
||||||
|
|
||||||
@ -76,6 +78,28 @@ class BulkRenameForm(forms.Form):
|
|||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Fields
|
||||||
|
#
|
||||||
|
|
||||||
|
class MACAddressField(forms.Field):
|
||||||
|
widget = forms.CharField
|
||||||
|
default_error_messages = {
|
||||||
|
'invalid': 'MAC address must be in EUI-48 format',
|
||||||
|
}
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
value = super().to_python(value)
|
||||||
|
|
||||||
|
# Validate MAC address format
|
||||||
|
try:
|
||||||
|
value = EUI(value.strip())
|
||||||
|
except AddrFormatError:
|
||||||
|
raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Regions
|
# Regions
|
||||||
#
|
#
|
||||||
@ -954,6 +978,16 @@ class PowerPortTemplateCreateForm(ComponentForm):
|
|||||||
name_pattern = ExpandableNameField(
|
name_pattern = ExpandableNameField(
|
||||||
label='Name'
|
label='Name'
|
||||||
)
|
)
|
||||||
|
maximum_draw = forms.IntegerField(
|
||||||
|
min_value=1,
|
||||||
|
required=False,
|
||||||
|
help_text="Maximum current draw (watts)"
|
||||||
|
)
|
||||||
|
allocated_draw = forms.IntegerField(
|
||||||
|
min_value=1,
|
||||||
|
required=False,
|
||||||
|
help_text="Allocated current draw (watts)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
class PowerOutletTemplateForm(BootstrapMixin, forms.ModelForm):
|
||||||
@ -1244,7 +1278,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
|
|||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url='/api/dcim/racks/',
|
api_url='/api/dcim/racks/',
|
||||||
display_field='display_name',
|
display_field='display_name'
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
position = forms.TypedChoiceField(
|
position = forms.TypedChoiceField(
|
||||||
@ -3614,7 +3648,7 @@ class PowerFeedBulkEditForm(BootstrapMixin, AddRemoveTagsForm, CustomFieldBulkEd
|
|||||||
queryset=PowerPanel.objects.all(),
|
queryset=PowerPanel.objects.all(),
|
||||||
required=False,
|
required=False,
|
||||||
widget=APISelect(
|
widget=APISelect(
|
||||||
api_url="/api/dcim/sites",
|
api_url="/api/dcim/power-panels/",
|
||||||
filter_for={
|
filter_for={
|
||||||
'rackgroup': 'site_id',
|
'rackgroup': 'site_id',
|
||||||
}
|
}
|
||||||
|
@ -2271,7 +2271,7 @@ class Interface(CableTermination, ComponentModel):
|
|||||||
|
|
||||||
# It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
|
# It's possible that an Interface can be deleted _after_ its parent Device/VM, in which case trying to resolve
|
||||||
# the component parent will raise DoesNotExist. For more discussion, see
|
# the component parent will raise DoesNotExist. For more discussion, see
|
||||||
# https://github.com/digitalocean/netbox/issues/2323
|
# https://github.com/netbox-community/netbox/issues/2323
|
||||||
try:
|
try:
|
||||||
parent_obj = self.device or self.virtual_machine
|
parent_obj = self.device or self.virtual_machine
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
@ -2772,6 +2772,16 @@ class Cable(ChangeLoggedModel):
|
|||||||
self.termination_a_type, self.termination_b_type
|
self.termination_a_type, self.termination_b_type
|
||||||
))
|
))
|
||||||
|
|
||||||
|
# A component with multiple positions must be connected to a component with an equal number of positions
|
||||||
|
term_a_positions = getattr(self.termination_a, 'positions', 1)
|
||||||
|
term_b_positions = getattr(self.termination_b, 'positions', 1)
|
||||||
|
if term_a_positions != term_b_positions:
|
||||||
|
raise ValidationError(
|
||||||
|
"{} has {} positions and {} has {}. Both terminations must have the same number of positions.".format(
|
||||||
|
self.termination_a, term_a_positions, self.termination_b, term_b_positions
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# A termination point cannot be connected to itself
|
# A termination point cannot be connected to itself
|
||||||
if self.termination_a == self.termination_b:
|
if self.termination_a == self.termination_b:
|
||||||
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
raise ValidationError("Cannot connect {} to itself".format(self.termination_a_type))
|
||||||
|
@ -424,7 +424,7 @@ class PowerPortTemplateTable(BaseTable):
|
|||||||
|
|
||||||
class Meta(BaseTable.Meta):
|
class Meta(BaseTable.Meta):
|
||||||
model = PowerPortTemplate
|
model = PowerPortTemplate
|
||||||
fields = ('pk', 'name')
|
fields = ('pk', 'name', 'maximum_draw', 'allocated_draw')
|
||||||
empty_text = "None"
|
empty_text = "None"
|
||||||
|
|
||||||
|
|
||||||
|
@ -1903,7 +1903,7 @@ class PowerConnectionsListView(PermissionRequiredMixin, ObjectListView):
|
|||||||
|
|
||||||
|
|
||||||
class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
|
class InterfaceConnectionsListView(PermissionRequiredMixin, ObjectListView):
|
||||||
permission_required = 'dcim.interface'
|
permission_required = 'dcim.view_interface'
|
||||||
queryset = Interface.objects.select_related(
|
queryset = Interface.objects.select_related(
|
||||||
'device', 'cable', '_connected_interface__device'
|
'device', 'cable', '_connected_interface__device'
|
||||||
).filter(
|
).filter(
|
||||||
|
@ -146,7 +146,7 @@ class ConfigContextDeleteView(PermissionRequiredMixin, ObjectDeleteView):
|
|||||||
|
|
||||||
|
|
||||||
class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
class ConfigContextBulkDeleteView(PermissionRequiredMixin, BulkDeleteView):
|
||||||
permission_required = 'extras.delete_cconfigcontext'
|
permission_required = 'extras.delete_configcontext'
|
||||||
queryset = ConfigContext.objects.all()
|
queryset = ConfigContext.objects.all()
|
||||||
table = ConfigContextTable
|
table = ConfigContextTable
|
||||||
default_return_url = 'extras:configcontext_list'
|
default_return_url = 'extras:configcontext_list'
|
||||||
@ -228,6 +228,13 @@ class ObjectChangeLogView(View):
|
|||||||
orderable=False
|
orderable=False
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Apply the request context
|
||||||
|
paginate = {
|
||||||
|
'paginator_class': EnhancedPaginator,
|
||||||
|
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||||
|
}
|
||||||
|
RequestConfig(request, paginate).configure(objectchanges_table)
|
||||||
|
|
||||||
# Check whether a header template exists for this model
|
# Check whether a header template exists for this model
|
||||||
base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
|
base_template = '{}/{}.html'.format(model._meta.app_label, model._meta.model_name)
|
||||||
try:
|
try:
|
||||||
@ -239,7 +246,7 @@ class ObjectChangeLogView(View):
|
|||||||
|
|
||||||
return render(request, 'extras/object_changelog.html', {
|
return render(request, 'extras/object_changelog.html', {
|
||||||
object_var: obj,
|
object_var: obj,
|
||||||
'objectchanges_table': objectchanges_table,
|
'table': objectchanges_table,
|
||||||
'base_template': base_template,
|
'base_template': base_template,
|
||||||
'active_tab': 'changelog',
|
'active_tab': 'changelog',
|
||||||
})
|
})
|
||||||
|
@ -12,7 +12,7 @@ from django.core.exceptions import ImproperlyConfigured
|
|||||||
# Environment setup
|
# Environment setup
|
||||||
#
|
#
|
||||||
|
|
||||||
VERSION = '2.6.1'
|
VERSION = '2.6.2'
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
HOSTNAME = platform.node()
|
HOSTNAME = platform.node()
|
||||||
|
@ -14,7 +14,7 @@ schema_view = get_schema_view(
|
|||||||
title="NetBox API",
|
title="NetBox API",
|
||||||
default_version='v2',
|
default_version='v2',
|
||||||
description="API to access NetBox",
|
description="API to access NetBox",
|
||||||
terms_of_service="https://github.com/digitalocean/netbox",
|
terms_of_service="https://github.com/netbox-community/netbox",
|
||||||
contact=openapi.Contact(email="netbox@digitalocean.com"),
|
contact=openapi.Contact(email="netbox@digitalocean.com"),
|
||||||
license=openapi.License(name="Apache v2 License"),
|
license=openapi.License(name="Apache v2 License"),
|
||||||
),
|
),
|
||||||
|
@ -15,7 +15,7 @@ from dcim.filters import (
|
|||||||
VirtualChassisFilter,
|
VirtualChassisFilter,
|
||||||
)
|
)
|
||||||
from dcim.models import (
|
from dcim.models import (
|
||||||
Cable, ConsolePort, Device, DeviceType, Interface, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis
|
Cable, ConsolePort, Device, DeviceType, Interface, PowerPanel, PowerFeed, PowerPort, Rack, RackGroup, Site, VirtualChassis
|
||||||
)
|
)
|
||||||
from dcim.tables import (
|
from dcim.tables import (
|
||||||
CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
|
CableTable, DeviceDetailTable, DeviceTypeTable, PowerFeedTable, RackTable, RackGroupTable, SiteTable,
|
||||||
@ -196,6 +196,7 @@ class HomeView(View):
|
|||||||
'cable_count': cables.count(),
|
'cable_count': cables.count(),
|
||||||
'console_connections_count': connected_consoleports.count(),
|
'console_connections_count': connected_consoleports.count(),
|
||||||
'power_connections_count': connected_powerports.count(),
|
'power_connections_count': connected_powerports.count(),
|
||||||
|
'powerpanel_count': PowerPanel.objects.count(),
|
||||||
'powerfeed_count': PowerFeed.objects.count(),
|
'powerfeed_count': PowerFeed.objects.count(),
|
||||||
|
|
||||||
# IPAM
|
# IPAM
|
||||||
|
@ -183,7 +183,7 @@ $(document).ready(function() {
|
|||||||
// Additional query params
|
// Additional query params
|
||||||
$.each(element.attributes, function(index, attr){
|
$.each(element.attributes, function(index, attr){
|
||||||
if (attr.name.includes("data-additional-query-param-")){
|
if (attr.name.includes("data-additional-query-param-")){
|
||||||
var param_name = attr.name.split("data-additional-query-param-")[1]
|
var param_name = attr.name.split("data-additional-query-param-")[1];
|
||||||
parameters[param_name] = attr.value;
|
parameters[param_name] = attr.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -194,6 +194,8 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
processResults: function (data) {
|
processResults: function (data) {
|
||||||
var element = this.$element[0];
|
var element = this.$element[0];
|
||||||
|
// Clear any disabled options
|
||||||
|
$(element).children('option').attr('disabled', false);
|
||||||
var results = $.map(data.results, function (obj) {
|
var results = $.map(data.results, function (obj) {
|
||||||
obj.text = obj[element.getAttribute('display-field')] || obj.name;
|
obj.text = obj[element.getAttribute('display-field')] || obj.name;
|
||||||
obj.id = obj[element.getAttribute('value-field')] || obj.id;
|
obj.id = obj[element.getAttribute('value-field')] || obj.id;
|
||||||
@ -207,7 +209,7 @@ $(document).ready(function() {
|
|||||||
|
|
||||||
// Handle the null option, but only add it once
|
// Handle the null option, but only add it once
|
||||||
if (element.getAttribute('data-null-option') && data.previous === null) {
|
if (element.getAttribute('data-null-option') && data.previous === null) {
|
||||||
var null_option = $(element).children()[0]
|
var null_option = $(element).children()[0];
|
||||||
results.unshift({
|
results.unshift({
|
||||||
id: null_option.value,
|
id: null_option.value,
|
||||||
text: null_option.text
|
text: null_option.text
|
||||||
|
@ -58,8 +58,8 @@
|
|||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/">Docs</a> ·
|
<i class="fa fa-fw fa-book text-primary"></i> <a href="http://netbox.readthedocs.io/">Docs</a> ·
|
||||||
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> ·
|
<i class="fa fa-fw fa-cloud text-primary"></i> <a href="{% url 'api_docs' %}">API</a> ·
|
||||||
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/digitalocean/netbox">Code</a> ·
|
<i class="fa fa-fw fa-code text-primary"></i> <a href="https://github.com/netbox-community/netbox">Code</a> ·
|
||||||
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/digitalocean/netbox/wiki">Help</a>
|
<i class="fa fa-fw fa-support text-primary"></i> <a href="https://github.com/netbox-community/netbox/wiki">Help</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,10 +4,11 @@
|
|||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if obj %}<h1>{{ obj }}</h1>{% endif %}
|
{% if obj %}<h1>{{ obj }}</h1>{% endif %}
|
||||||
{% include 'panel_table.html' with table=objectchanges_table %}
|
{% include 'panel_table.html' %}
|
||||||
|
{% include 'inc/paginator.html' with paginator=table.paginator page=table.page %}
|
||||||
{% if settings.CHANGELOG_RETENTION %}
|
{% if settings.CHANGELOG_RETENTION %}
|
||||||
<div class="pull-right text-muted">
|
<div class="text-muted">
|
||||||
Changelog retention: {{ settings.CHANGELOG_RETENTION }} days
|
Changelog retention: {% if settings.CHANGELOG_RETENTION == 0 %}Indefinite{% else %}{{ settings.CHANGELOG_RETENTION }} days{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
{% include 'utilities/obj_table.html' %}
|
{% include 'utilities/obj_table.html' %}
|
||||||
{% if settings.CHANGELOG_RETENTION %}
|
{% if settings.CHANGELOG_RETENTION %}
|
||||||
<div class="pull-right text-muted">
|
<div class="pull-right text-muted">
|
||||||
Changelog retention: {{ settings.CHANGELOG_RETENTION }} days
|
Changelog retention: {% if settings.CHANGELOG_RETENTION == 0 %}Indefinite{% else %}{{ settings.CHANGELOG_RETENTION }} days{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
@ -115,6 +115,16 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="list-group-item-text text-muted">Electrical circuits delivering power from panels</p>
|
<p class="list-group-item-text text-muted">Electrical circuits delivering power from panels</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="list-group-item">
|
||||||
|
{% if perms.dcim.view_powerpanel %}
|
||||||
|
<span class="badge pull-right">{{ stats.powerpanel_count }}</span>
|
||||||
|
<h4 class="list-group-item-heading"><a href="{% url 'dcim:powerpanel_list' %}">Power Panels</a></h4>
|
||||||
|
{% else %}
|
||||||
|
<span class="badge pull-right"><i class="fa fa-lock"></i></span>
|
||||||
|
<h4 class="list-group-item-heading">Power Panels</h4>
|
||||||
|
{% endif %}
|
||||||
|
<p class="list-group-item-text text-muted">Electrical panels receiving utility power</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel panel-default">
|
<div class="panel panel-default">
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
Edit this cluster
|
Edit this cluster
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if perms.dcim.delete_cluster %}
|
{% if perms.virtualization.delete_cluster %}
|
||||||
<a href="{% url 'virtualization:cluster_delete' pk=cluster.pk %}" class="btn btn-danger">
|
<a href="{% url 'virtualization:cluster_delete' pk=cluster.pk %}" class="btn btn-danger">
|
||||||
<span class="fa fa-trash" aria-hidden="true"></span>
|
<span class="fa fa-trash" aria-hidden="true"></span>
|
||||||
Delete this cluster
|
Delete this cluster
|
||||||
|
@ -3,6 +3,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from dcim.forms import MACAddressField
|
||||||
from extras.models import Tag
|
from extras.models import Tag
|
||||||
|
|
||||||
|
|
||||||
@ -49,6 +50,14 @@ class MultiValueTimeFilter(django_filters.MultipleChoiceFilter):
|
|||||||
field_class = multivalue_field_factory(forms.TimeField)
|
field_class = multivalue_field_factory(forms.TimeField)
|
||||||
|
|
||||||
|
|
||||||
|
class MACAddressFilter(django_filters.CharFilter):
|
||||||
|
field_class = MACAddressField
|
||||||
|
|
||||||
|
|
||||||
|
class MultiValueMACAddressFilter(django_filters.MultipleChoiceFilter):
|
||||||
|
field_class = multivalue_field_factory(MACAddressField)
|
||||||
|
|
||||||
|
|
||||||
class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
class TreeNodeMultipleChoiceFilter(django_filters.ModelMultipleChoiceFilter):
|
||||||
"""
|
"""
|
||||||
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
|
Filters for a set of Models, including all descendant models within a Tree. Example: [<Region: R1>,<Region: R2>]
|
||||||
|
@ -10,12 +10,6 @@ cd "$(dirname "$0")"
|
|||||||
PYTHON="python3"
|
PYTHON="python3"
|
||||||
PIP="pip3"
|
PIP="pip3"
|
||||||
|
|
||||||
# TODO: Remove this in v2.6 as it is no longer needed under Python 3
|
|
||||||
# Delete stale bytecode
|
|
||||||
COMMAND="find . -name \"*.pyc\" -delete"
|
|
||||||
echo "Cleaning up stale Python bytecode ($COMMAND)..."
|
|
||||||
eval $COMMAND
|
|
||||||
|
|
||||||
# Uninstall any Python packages which are no longer needed
|
# Uninstall any Python packages which are no longer needed
|
||||||
COMMAND="${PIP} uninstall -r old_requirements.txt -y"
|
COMMAND="${PIP} uninstall -r old_requirements.txt -y"
|
||||||
echo "Removing old Python packages ($COMMAND)..."
|
echo "Removing old Python packages ($COMMAND)..."
|
||||||
|
Loading…
Reference in New Issue
Block a user