mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-21 11:37:21 -06:00
commit
a85e6370a8
49
.github/ISSUE_TEMPLATE.md
vendored
49
.github/ISSUE_TEMPLATE.md
vendored
@ -1,49 +0,0 @@
|
|||||||
<!--
|
|
||||||
Before opening a new issue, please search through the existing issues to
|
|
||||||
see if your topic has already been addressed. Note that you may need to
|
|
||||||
remove the "is:open" filter from the search bar to include closed issues.
|
|
||||||
|
|
||||||
Check the appropriate type for your issue below by placing an x between the
|
|
||||||
brackets. For assistance with installation issues, or for any other issues
|
|
||||||
other than those listed below, please raise your topic for discussion on
|
|
||||||
our mailing list:
|
|
||||||
|
|
||||||
https://groups.google.com/forum/#!forum/netbox-discuss
|
|
||||||
|
|
||||||
Please note that issues which do not fall under any of the below categories
|
|
||||||
will be closed. Due to an excessive backlog of feature requests, we are
|
|
||||||
not currently accepting any proposals which extend NetBox's feature scope.
|
|
||||||
|
|
||||||
Do not prepend any sort of tag to your issue's title. An administrator will
|
|
||||||
review your issue and assign labels as appropriate.
|
|
||||||
--->
|
|
||||||
### Issue type
|
|
||||||
[ ] Feature request <!-- An enhancement of existing functionality -->
|
|
||||||
[ ] Bug report <!-- Unexpected or erroneous behavior -->
|
|
||||||
[ ] Documentation <!-- A modification to the documentation -->
|
|
||||||
[ ] Housekeeping <!-- Changes pertaining to the codebase itself -->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Please describe the environment in which you are running NetBox. (Be sure
|
|
||||||
to verify that you are running the latest stable release of NetBox before
|
|
||||||
submitting a bug report.) If you are submitting a bug report and have made
|
|
||||||
any changes to the code base, please first validate that your bug can be
|
|
||||||
recreated while running an official release.
|
|
||||||
-->
|
|
||||||
### Environment
|
|
||||||
* Python version: <!-- Example: 3.5.4 -->
|
|
||||||
* NetBox version: <!-- Example: 2.3.5 -->
|
|
||||||
|
|
||||||
<!--
|
|
||||||
BUG REPORTS must include:
|
|
||||||
* A list of the steps needed for someone else to reproduce the bug
|
|
||||||
* A description of the expected and observed behavior
|
|
||||||
* Any relevant error messages (screenshots may also help)
|
|
||||||
|
|
||||||
FEATURE REQUESTS must include:
|
|
||||||
* A detailed description of the proposed functionality
|
|
||||||
* A use case for the new feature
|
|
||||||
* A rough description of any necessary changes to the database schema
|
|
||||||
* Any relevant third-party libraries which would be needed
|
|
||||||
-->
|
|
||||||
### Description
|
|
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
34
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
name: :bug: Bug Report
|
||||||
|
about: Report a reproducible bug in the current release of NetBox
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
NOTE: This form is only for reproducible bugs. If you need assistance with
|
||||||
|
NetBox installation, or if you have a general question, DO NOT open an
|
||||||
|
issue. Instead, post to our mailing list:
|
||||||
|
|
||||||
|
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||||
|
|
||||||
|
Please describe the environment in which you are running NetBox. Be sure
|
||||||
|
that you are running an unmodified instance of the latest stable release
|
||||||
|
before submitting a bug report.
|
||||||
|
-->
|
||||||
|
### Environment
|
||||||
|
* Python version: <!-- Example: 3.5.4 -->
|
||||||
|
* NetBox version: <!-- Example: 2.3.6 -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe in detail the steps that someone else can take to reproduce this
|
||||||
|
bug using the current stable release of NetBox (or the current beta release
|
||||||
|
where applicable).
|
||||||
|
-->
|
||||||
|
### Steps to Reproduce
|
||||||
|
|
||||||
|
|
||||||
|
<!-- What did you expect to happen? -->
|
||||||
|
### Expected Behavior
|
||||||
|
|
||||||
|
|
||||||
|
<!-- What happened instead? -->
|
||||||
|
### Observed Behavior
|
17
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/documentation_change.md
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: :book: Documentation Change
|
||||||
|
about: Suggest an addition or modification to the NetBox documentation
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Please indicate the nature of the change by placing an X in one of the
|
||||||
|
boxes below.
|
||||||
|
-->
|
||||||
|
### Change Type
|
||||||
|
[ ] Addition
|
||||||
|
[ ] Correction
|
||||||
|
[ ] Deprecation
|
||||||
|
[ ] Cleanup (formatting, typos, etc.)
|
||||||
|
|
||||||
|
<!-- Describe the proposed change(s). -->
|
||||||
|
### Proposed Changes
|
53
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
53
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
---
|
||||||
|
name: :new: Feature Request
|
||||||
|
about: Propose a new NetBox feature or enhancement
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
NOTE: This form is only for proposing specific new features or enhancements.
|
||||||
|
If you have a general idea or question, please post to our mailing list
|
||||||
|
instead of opening an issue:
|
||||||
|
|
||||||
|
https://groups.google.com/forum/#!forum/netbox-discuss
|
||||||
|
|
||||||
|
NOTE: Due to an excessive backlog of feature requests, we are not currently
|
||||||
|
accepting any proposals which significantly extend NetBox's feature scope.
|
||||||
|
|
||||||
|
Please describe the environment in which you are running NetBox. Be sure
|
||||||
|
that you are running an unmodified instance of the latest stable release
|
||||||
|
before submitting a bug report.
|
||||||
|
-->
|
||||||
|
### Environment
|
||||||
|
* Python version: <!-- Example: 3.5.4 -->
|
||||||
|
* NetBox version: <!-- Example: 2.3.6 -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Describe in detail the new functionality you are proposing. Include any
|
||||||
|
specific changes to work flows, data models, or the user interface.
|
||||||
|
-->
|
||||||
|
### Proposed Functionality
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Convey an example use case for your proposed feature. Write from the
|
||||||
|
perspective of a NetBox user who would benefit from the proposed
|
||||||
|
functionality and describe how.
|
||||||
|
--->
|
||||||
|
### Use Case
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Note any changes to the database schema necessary to support the new
|
||||||
|
feature. For example, does the proposal require adding a new model or
|
||||||
|
field? (Not all new features require database changes.)
|
||||||
|
--->
|
||||||
|
### Database Changes
|
||||||
|
|
||||||
|
|
||||||
|
<!--
|
||||||
|
List any new dependencies on external libraries or services that this new
|
||||||
|
feature would introduce. For example, does the proposal require the
|
||||||
|
installation of a new Python package? (Not all new features introduce new
|
||||||
|
dependencies.)
|
||||||
|
-->
|
||||||
|
### External Dependencies
|
16
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
Normal file
16
.github/ISSUE_TEMPLATE/housekeeping.md
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: :house: Housekeeping
|
||||||
|
about: A change pertaining to the codebase itself
|
||||||
|
---
|
||||||
|
|
||||||
|
<!--
|
||||||
|
NOTE: This type of issue should be opened only by those reasonably familiar
|
||||||
|
with NetBox's code base and interested in contributing to its development.
|
||||||
|
|
||||||
|
Describe the proposed change(s) in detail.
|
||||||
|
-->
|
||||||
|
### Proposed Changes
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Provide justification for the proposed change(s). -->
|
||||||
|
### Justification -->
|
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -6,6 +6,8 @@
|
|||||||
be able to accept.
|
be able to accept.
|
||||||
|
|
||||||
Please indicate the relevant feature request or bug report below.
|
Please indicate the relevant feature request or bug report below.
|
||||||
|
IF YOUR PULL REQUEST DOES NOT REFERENCE AN ACCEPTED BUG REPORT OR
|
||||||
|
FEATURE REQUEST, IT WILL BE MARKED AS INVALID AND CLOSED.
|
||||||
-->
|
-->
|
||||||
### Fixes:
|
### Fixes:
|
||||||
|
|
||||||
|
@ -91,11 +91,13 @@ appropriate labels will be applied for categorization.
|
|||||||
|
|
||||||
## Submitting Pull Requests
|
## Submitting Pull Requests
|
||||||
|
|
||||||
* Be sure to open an issue before starting work on a pull request, and discuss
|
* Be sure to open an issue **before** starting work on a pull request, and
|
||||||
your idea with the NetBox maintainers before beginning work. This will help
|
discuss your idea with the NetBox maintainers before beginning work. This will
|
||||||
prevent wasting time on something that might we might not be able to implement.
|
help prevent wasting time on something that might we might not be able to
|
||||||
When suggesting a new feature, also make sure it won't conflict with any work
|
implement. When suggesting a new feature, also make sure it won't conflict with
|
||||||
that's already in progress.
|
any work that's already in progress.
|
||||||
|
|
||||||
|
* Any pull request which does _not_ relate to an accepted issue will be closed.
|
||||||
|
|
||||||
* When submitting a pull request, please be sure to work off of the `develop`
|
* When submitting a pull request, please be sure to work off of the `develop`
|
||||||
branch, rather than `master`. The `develop` branch is used for ongoing
|
branch, rather than `master`. The `develop` branch is used for ongoing
|
||||||
|
@ -267,7 +267,7 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
import napalm
|
import napalm
|
||||||
except ImportError:
|
except ImportError:
|
||||||
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
raise ServiceUnavailable("NAPALM is not installed. Please see the documentation for instructions.")
|
||||||
from napalm.base.exceptions import ConnectAuthError, ModuleImportError
|
from napalm.base.exceptions import ModuleImportError
|
||||||
|
|
||||||
# Validate the configured driver
|
# Validate the configured driver
|
||||||
try:
|
try:
|
||||||
@ -281,16 +281,8 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
if not request.user.has_perm('dcim.napalm_read'):
|
if not request.user.has_perm('dcim.napalm_read'):
|
||||||
return HttpResponseForbidden()
|
return HttpResponseForbidden()
|
||||||
|
|
||||||
# Validate requested NAPALM methods
|
# Connect to the device
|
||||||
napalm_methods = request.GET.getlist('method')
|
napalm_methods = request.GET.getlist('method')
|
||||||
for method in napalm_methods:
|
|
||||||
if not hasattr(driver, method):
|
|
||||||
return HttpResponseBadRequest("Unknown NAPALM method: {}".format(method))
|
|
||||||
elif not method.startswith('get_'):
|
|
||||||
return HttpResponseBadRequest("Unsupported NAPALM method: {}".format(method))
|
|
||||||
|
|
||||||
# Connect to the device and execute the requested methods
|
|
||||||
# TODO: Improve error handling
|
|
||||||
response = OrderedDict([(m, None) for m in napalm_methods])
|
response = OrderedDict([(m, None) for m in napalm_methods])
|
||||||
ip_address = str(device.primary_ip.address.ip)
|
ip_address = str(device.primary_ip.address.ip)
|
||||||
d = driver(
|
d = driver(
|
||||||
@ -302,12 +294,23 @@ class DeviceViewSet(CustomFieldModelViewSet):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
d.open()
|
d.open()
|
||||||
for method in napalm_methods:
|
|
||||||
response[method] = getattr(d, method)()
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
|
raise ServiceUnavailable("Error connecting to the device at {}: {}".format(ip_address, e))
|
||||||
|
|
||||||
|
# Validate and execute each specified NAPALM method
|
||||||
|
for method in napalm_methods:
|
||||||
|
if not hasattr(driver, method):
|
||||||
|
response[method] = {'error': 'Unknown NAPALM method'}
|
||||||
|
continue
|
||||||
|
if not method.startswith('get_'):
|
||||||
|
response[method] = {'error': 'Only get_* NAPALM methods are supported'}
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
response[method] = getattr(d, method)()
|
||||||
|
except NotImplementedError:
|
||||||
|
response[method] = {'error': 'Method not implemented for NAPALM driver {}'.format(driver)}
|
||||||
d.close()
|
d.close()
|
||||||
|
|
||||||
return Response(response)
|
return Response(response)
|
||||||
|
|
||||||
|
|
||||||
|
@ -509,7 +509,7 @@ class DeviceFilter(CustomFieldFilterSet, django_filters.FilterSet):
|
|||||||
Q(name__icontains=value) |
|
Q(name__icontains=value) |
|
||||||
Q(serial__icontains=value.strip()) |
|
Q(serial__icontains=value.strip()) |
|
||||||
Q(inventory_items__serial__icontains=value.strip()) |
|
Q(inventory_items__serial__icontains=value.strip()) |
|
||||||
Q(asset_tag=value.strip()) |
|
Q(asset_tag__icontains=value.strip()) |
|
||||||
Q(comments__icontains=value)
|
Q(comments__icontains=value)
|
||||||
).distinct()
|
).distinct()
|
||||||
|
|
||||||
|
@ -781,9 +781,6 @@ class DeviceRole(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
def get_absolute_url(self):
|
|
||||||
return "{}?role={}".format(reverse('dcim:device_list'), self.slug)
|
|
||||||
|
|
||||||
def to_csv(self):
|
def to_csv(self):
|
||||||
return (
|
return (
|
||||||
self.name,
|
self.name,
|
||||||
|
@ -11,13 +11,8 @@ def assign_virtualchassis_master(instance, created, **kwargs):
|
|||||||
"""
|
"""
|
||||||
When a VirtualChassis is created, automatically assign its master device to the VC.
|
When a VirtualChassis is created, automatically assign its master device to the VC.
|
||||||
"""
|
"""
|
||||||
# Default to 1 but don't overwrite an existing position (see #2087)
|
|
||||||
if instance.master.vc_position is not None:
|
|
||||||
vc_position = instance.master.vc_position
|
|
||||||
else:
|
|
||||||
vc_position = 1
|
|
||||||
if created:
|
if created:
|
||||||
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=vc_position)
|
Device.objects.filter(pk=instance.master.pk).update(virtual_chassis=instance, vc_position=None)
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=VirtualChassis)
|
@receiver(pre_delete, sender=VirtualChassis)
|
||||||
|
@ -408,7 +408,6 @@ class DeviceBayTemplateTable(BaseTable):
|
|||||||
|
|
||||||
class DeviceRoleTable(BaseTable):
|
class DeviceRoleTable(BaseTable):
|
||||||
pk = ToggleColumn()
|
pk = ToggleColumn()
|
||||||
name = tables.LinkColumn(verbose_name='Name')
|
|
||||||
device_count = tables.TemplateColumn(
|
device_count = tables.TemplateColumn(
|
||||||
template_code=DEVICEROLE_DEVICE_COUNT,
|
template_code=DEVICEROLE_DEVICE_COUNT,
|
||||||
accessor=Accessor('devices.count'),
|
accessor=Accessor('devices.count'),
|
||||||
|
@ -196,8 +196,9 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
|||||||
|
|
||||||
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
|
# Assign addresses from the list of available IPs and copy VRF assignment from the parent prefix
|
||||||
available_ips = iter(available_ips)
|
available_ips = iter(available_ips)
|
||||||
|
prefix_length = prefix.prefix.prefixlen
|
||||||
for requested_ip in requested_ips:
|
for requested_ip in requested_ips:
|
||||||
requested_ip['address'] = next(available_ips)
|
requested_ip['address'] = '{}/{}'.format(next(available_ips), prefix_length)
|
||||||
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
requested_ip['vrf'] = prefix.vrf.pk if prefix.vrf else None
|
||||||
|
|
||||||
# Initialize the serializer with a list or a single object depending on what was requested
|
# Initialize the serializer with a list or a single object depending on what was requested
|
||||||
|
@ -194,17 +194,35 @@ class RIRTable(BaseTable):
|
|||||||
|
|
||||||
|
|
||||||
class RIRDetailTable(RIRTable):
|
class RIRDetailTable(RIRTable):
|
||||||
stats_total = tables.Column(accessor='stats.total', verbose_name='Total',
|
stats_total = tables.Column(
|
||||||
footer=lambda table: sum(r.stats['total'] for r in table.data))
|
accessor='stats.total',
|
||||||
stats_active = tables.Column(accessor='stats.active', verbose_name='Active',
|
verbose_name='Total',
|
||||||
footer=lambda table: sum(r.stats['active'] for r in table.data))
|
footer=lambda table: sum(r.stats['total'] for r in table.data)
|
||||||
stats_reserved = tables.Column(accessor='stats.reserved', verbose_name='Reserved',
|
)
|
||||||
footer=lambda table: sum(r.stats['reserved'] for r in table.data))
|
stats_active = tables.Column(
|
||||||
stats_deprecated = tables.Column(accessor='stats.deprecated', verbose_name='Deprecated',
|
accessor='stats.active',
|
||||||
footer=lambda table: sum(r.stats['deprecated'] for r in table.data))
|
verbose_name='Active',
|
||||||
stats_available = tables.Column(accessor='stats.available', verbose_name='Available',
|
footer=lambda table: sum(r.stats['active'] for r in table.data)
|
||||||
footer=lambda table: sum(r.stats['available'] for r in table.data))
|
)
|
||||||
utilization = tables.TemplateColumn(template_code=RIR_UTILIZATION, verbose_name='Utilization')
|
stats_reserved = tables.Column(
|
||||||
|
accessor='stats.reserved',
|
||||||
|
verbose_name='Reserved',
|
||||||
|
footer=lambda table: sum(r.stats['reserved'] for r in table.data)
|
||||||
|
)
|
||||||
|
stats_deprecated = tables.Column(
|
||||||
|
accessor='stats.deprecated',
|
||||||
|
verbose_name='Deprecated',
|
||||||
|
footer=lambda table: sum(r.stats['deprecated'] for r in table.data)
|
||||||
|
)
|
||||||
|
stats_available = tables.Column(
|
||||||
|
accessor='stats.available',
|
||||||
|
verbose_name='Available',
|
||||||
|
footer=lambda table: sum(r.stats['available'] for r in table.data)
|
||||||
|
)
|
||||||
|
utilization = tables.TemplateColumn(
|
||||||
|
template_code=RIR_UTILIZATION,
|
||||||
|
verbose_name='Utilization'
|
||||||
|
)
|
||||||
|
|
||||||
class Meta(RIRTable.Meta):
|
class Meta(RIRTable.Meta):
|
||||||
fields = (
|
fields = (
|
||||||
|
@ -192,9 +192,15 @@ class RIRListView(ObjectListView):
|
|||||||
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
|
queryset = Prefix.objects.filter(prefix__net_contained_or_equal=str(aggregate.prefix))
|
||||||
|
|
||||||
# Find all consumed space for each prefix status (we ignore containers for this purpose).
|
# Find all consumed space for each prefix status (we ignore containers for this purpose).
|
||||||
active_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)])
|
active_prefixes = netaddr.cidr_merge(
|
||||||
reserved_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)])
|
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_ACTIVE)]
|
||||||
deprecated_prefixes = netaddr.cidr_merge([p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)])
|
)
|
||||||
|
reserved_prefixes = netaddr.cidr_merge(
|
||||||
|
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_RESERVED)]
|
||||||
|
)
|
||||||
|
deprecated_prefixes = netaddr.cidr_merge(
|
||||||
|
[p.prefix for p in queryset.filter(status=PREFIX_STATUS_DEPRECATED)]
|
||||||
|
)
|
||||||
|
|
||||||
# Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
|
# Find all available prefixes by subtracting each of the existing prefix sets from the aggregate prefix.
|
||||||
available_prefixes = (
|
available_prefixes = (
|
||||||
@ -205,11 +211,11 @@ class RIRListView(ObjectListView):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Add the size of each metric to the RIR total.
|
# Add the size of each metric to the RIR total.
|
||||||
stats['total'] += aggregate.prefix.size / denominator
|
stats['total'] += int(aggregate.prefix.size / denominator)
|
||||||
stats['active'] += netaddr.IPSet(active_prefixes).size / denominator
|
stats['active'] += int(netaddr.IPSet(active_prefixes).size / denominator)
|
||||||
stats['reserved'] += netaddr.IPSet(reserved_prefixes).size / denominator
|
stats['reserved'] += int(netaddr.IPSet(reserved_prefixes).size / denominator)
|
||||||
stats['deprecated'] += netaddr.IPSet(deprecated_prefixes).size / denominator
|
stats['deprecated'] += int(netaddr.IPSet(deprecated_prefixes).size / denominator)
|
||||||
stats['available'] += available_prefixes.size / denominator
|
stats['available'] += int(available_prefixes.size / denominator)
|
||||||
|
|
||||||
# Calculate the percentage of total space for each prefix status.
|
# Calculate the percentage of total space for each prefix status.
|
||||||
total = float(stats['total'])
|
total = float(stats['total'])
|
||||||
@ -229,20 +235,6 @@ class RIRListView(ObjectListView):
|
|||||||
|
|
||||||
return rirs
|
return rirs
|
||||||
|
|
||||||
def extra_context(self):
|
|
||||||
|
|
||||||
totals = {
|
|
||||||
'total': sum([rir.stats['total'] for rir in self.queryset]),
|
|
||||||
'active': sum([rir.stats['active'] for rir in self.queryset]),
|
|
||||||
'reserved': sum([rir.stats['reserved'] for rir in self.queryset]),
|
|
||||||
'deprecated': sum([rir.stats['deprecated'] for rir in self.queryset]),
|
|
||||||
'available': sum([rir.stats['available'] for rir in self.queryset]),
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'totals': totals,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
|
class RIRCreateView(PermissionRequiredMixin, ObjectEditView):
|
||||||
permission_required = 'ipam.add_rir'
|
permission_required = 'ipam.add_rir'
|
||||||
|
@ -22,7 +22,7 @@ if sys.version_info[0] < 3:
|
|||||||
DeprecationWarning
|
DeprecationWarning
|
||||||
)
|
)
|
||||||
|
|
||||||
VERSION = '2.3.6'
|
VERSION = '2.3.7'
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
@ -74,3 +74,5 @@ if settings.DEBUG:
|
|||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
|
url(r'^{}'.format(settings.BASE_PATH), include(_patterns))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
handler500 = 'utilities.views.server_error'
|
||||||
|
@ -372,12 +372,19 @@ table.reports td.method {
|
|||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
padding-left: 30px;
|
padding-left: 30px;
|
||||||
}
|
}
|
||||||
table.reports td.stats label {
|
td.report-stats label {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
line-height: 14px;
|
line-height: 14px;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
min-width: 40px;
|
min-width: 40px;
|
||||||
}
|
}
|
||||||
|
table.report th {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
table.report th a {
|
||||||
|
position: absolute;
|
||||||
|
top: -51px;
|
||||||
|
}
|
||||||
|
|
||||||
/* AJAX loader */
|
/* AJAX loader */
|
||||||
.loading {
|
.loading {
|
||||||
|
@ -153,7 +153,8 @@ class UserKeyForm(BootstrapMixin, forms.ModelForm):
|
|||||||
model = UserKey
|
model = UserKey
|
||||||
fields = ['public_key']
|
fields = ['public_key']
|
||||||
help_texts = {
|
help_texts = {
|
||||||
'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption.",
|
'public_key': "Enter your public RSA key. Keep the private one with you; you'll need it for decryption. "
|
||||||
|
"Please note that passphrase-protected keys are not supported.",
|
||||||
}
|
}
|
||||||
|
|
||||||
def clean_public_key(self):
|
def clean_public_key(self):
|
||||||
|
@ -146,7 +146,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>Role</td>
|
<td>Role</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ device.device_role.get_absolute_url }}">{{ device.device_role }}</a>
|
<a href="{% url 'dcim:device_list' %}?role={{ device.device_role.slug }}">{{ device.device_role }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -29,63 +29,73 @@
|
|||||||
<p class="lead">{{ report.description }}</p>
|
<p class="lead">{{ report.description }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if report.result %}
|
{% if report.result %}
|
||||||
<p>Last run: {{ report.result.created }}</p>
|
<p>Last run: <strong>{{ report.result.created }}</strong></p>
|
||||||
{% else %}
|
|
||||||
<p class="text-muted">Last run: Never</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
<div class="col-md-9">
|
|
||||||
{% if report.result %}
|
{% if report.result %}
|
||||||
<table class="table table-hover">
|
<div class="panel panel-default">
|
||||||
<thead>
|
<div class="panel-heading">
|
||||||
<tr>
|
<strong>Report Methods</strong>
|
||||||
<th>Time</th>
|
</div>
|
||||||
<th>Level</th>
|
<table class="table table-hover panel-body">
|
||||||
<th>Object</th>
|
{% for method, data in report.result.data.items %}
|
||||||
<th>Message</th>
|
<tr>
|
||||||
</tr>
|
<td><code><a href="#{{ method }}">{{ method }}</a></code></td>
|
||||||
</thead>
|
<td class="text-right report-stats">
|
||||||
{% for method, data in report.result.data.items %}
|
<label class="label label-success">{{ data.success }}</label>
|
||||||
<tr>
|
<label class="label label-info">{{ data.info }}</label>
|
||||||
<th colspan="4"><a name="{{ method }}"></a>{{ method }}</th>
|
<label class="label label-warning">{{ data.warning }}</label>
|
||||||
</tr>
|
<label class="label label-danger">{{ data.failure }}</label>
|
||||||
{% for time, level, obj, url, message in data.log %}
|
|
||||||
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
|
|
||||||
<td>{{ time }}</td>
|
|
||||||
<td>
|
|
||||||
<label class="label label-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
|
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
{% if obj and url %}
|
|
||||||
<a href="{{ url }}">{{ obj }}</a>
|
|
||||||
{% elif obj %}
|
|
||||||
{{ obj }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td>{{ message }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endfor %}
|
</table>
|
||||||
</table>
|
</div>
|
||||||
|
<div class="panel panel-default">
|
||||||
|
<div class="panel-heading">
|
||||||
|
<strong>Report Results</strong>
|
||||||
|
</div>
|
||||||
|
<table class="table table-hover panel-body report">
|
||||||
|
<thead>
|
||||||
|
<tr class="table-headings">
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Level</th>
|
||||||
|
<th>Object</th>
|
||||||
|
<th>Message</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for method, data in report.result.data.items %}
|
||||||
|
<tr>
|
||||||
|
<th colspan="4" style="font-family: monospace">
|
||||||
|
<a name="{{ method }}"></a>{{ method }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
{% for time, level, obj, url, message in data.log %}
|
||||||
|
<tr class="{% if level == 'failure' %}danger{% elif level %}{{ level }}{% endif %}">
|
||||||
|
<td>{{ time }}</td>
|
||||||
|
<td>
|
||||||
|
<label class="label label-{% if level == 'failure' %}danger{% else %}{{ level }}{% endif %}">{{ level|title }}</label>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if obj and url %}
|
||||||
|
<a href="{{ url }}">{{ obj }}</a>
|
||||||
|
{% elif obj %}
|
||||||
|
{{ obj }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>{{ message }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div class="well">No results are available for this report. Please run the report first.</div>
|
<div class="well">No results are available for this report. Please run the report first.</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
{% if report.result %}
|
{% if report.result %}
|
||||||
<div class="panel panel-default">
|
|
||||||
<div class="panel-heading">
|
|
||||||
<strong>Methods</strong>
|
|
||||||
</div>
|
|
||||||
<ul class="list-group">
|
|
||||||
{% for method, data in report.result.data.items %}
|
|
||||||
<li class="list-group-item">
|
|
||||||
<a href="#{{ method }}">{{ method }}</a>
|
|
||||||
<span class="badge">{{ data.log|length }}</span>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -38,7 +38,7 @@
|
|||||||
<td colspan="3" class="method">
|
<td colspan="3" class="method">
|
||||||
{{ method }}
|
{{ method }}
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right stats">
|
<td class="text-right report-stats">
|
||||||
<label class="label label-success">{{ stats.success }}</label>
|
<label class="label label-success">{{ stats.success }}</label>
|
||||||
<label class="label label-info">{{ stats.info }}</label>
|
<label class="label label-info">{{ stats.info }}</label>
|
||||||
<label class="label label-warning">{{ stats.warning }}</label>
|
<label class="label label-warning">{{ stats.warning }}</label>
|
||||||
@ -69,7 +69,7 @@
|
|||||||
<a href="#report.{{ report.name }}" class="list-group-item">
|
<a href="#report.{{ report.name }}" class="list-group-item">
|
||||||
<i class="fa fa-list-alt"></i> {{ report.name }}
|
<i class="fa fa-list-alt"></i> {{ report.name }}
|
||||||
<div class="pull-right">
|
<div class="pull-right">
|
||||||
{% include 'extras/inc/report_label.html' %}
|
{% include 'extras/inc/report_label.html' with result=report.result %}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -65,10 +65,11 @@
|
|||||||
<td>Tenant</td>
|
<td>Tenant</td>
|
||||||
<td>
|
<td>
|
||||||
{% if ipaddress.tenant %}
|
{% if ipaddress.tenant %}
|
||||||
|
{% if ipaddress.tenant.group %}
|
||||||
|
<a href="{{ ipaddress.tenant.group.get_absolute_url }}">{{ ipaddress.tenant.group }}</a>
|
||||||
|
<i class="fa fa-angle-right"></i>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ ipaddress.tenant.get_absolute_url }}">{{ ipaddress.tenant }}</a>
|
<a href="{{ ipaddress.tenant.get_absolute_url }}">{{ ipaddress.tenant }}</a>
|
||||||
{% elif ipaddress.vrf.tenant %}
|
|
||||||
<a href="{{ ipaddress.vrf.tenant.get_absolute_url }}">{{ ipaddress.vrf.tenant }}</a>
|
|
||||||
<label class="label label-info">Inherited</label>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -35,13 +35,6 @@
|
|||||||
<i class="fa fa-angle-right"></i>
|
<i class="fa fa-angle-right"></i>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
|
<a href="{{ prefix.tenant.get_absolute_url }}">{{ prefix.tenant }}</a>
|
||||||
{% elif prefix.vrf.tenant %}
|
|
||||||
{% if prefix.vrf.tenant.group %}
|
|
||||||
<a href="{{ prefix.vrf.tenant.group.get_absolute_url }}">{{ prefix.vrf.tenant.group }}</a>
|
|
||||||
<i class="fa fa-angle-right"></i>
|
|
||||||
{% endif %}
|
|
||||||
<a href="{{ prefix.vrf.tenant.get_absolute_url }}">{{ prefix.vrf.tenant }}</a>
|
|
||||||
<label class="label label-info">Inherited</label>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">None</span>
|
<span class="text-muted">None</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -78,14 +78,8 @@ class TenantView(View):
|
|||||||
'rackreservation_count': RackReservation.objects.filter(tenant=tenant).count(),
|
'rackreservation_count': RackReservation.objects.filter(tenant=tenant).count(),
|
||||||
'device_count': Device.objects.filter(tenant=tenant).count(),
|
'device_count': Device.objects.filter(tenant=tenant).count(),
|
||||||
'vrf_count': VRF.objects.filter(tenant=tenant).count(),
|
'vrf_count': VRF.objects.filter(tenant=tenant).count(),
|
||||||
'prefix_count': Prefix.objects.filter(
|
'prefix_count': Prefix.objects.filter(tenant=tenant).count(),
|
||||||
Q(tenant=tenant) |
|
'ipaddress_count': IPAddress.objects.filter(tenant=tenant).count(),
|
||||||
Q(tenant__isnull=True, vrf__tenant=tenant)
|
|
||||||
).count(),
|
|
||||||
'ipaddress_count': IPAddress.objects.filter(
|
|
||||||
Q(tenant=tenant) |
|
|
||||||
Q(tenant__isnull=True, vrf__tenant=tenant)
|
|
||||||
).count(),
|
|
||||||
'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
|
'vlan_count': VLAN.objects.filter(tenant=tenant).count(),
|
||||||
'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
|
'circuit_count': Circuit.objects.filter(tenant=tenant).count(),
|
||||||
'virtualmachine_count': VirtualMachine.objects.filter(tenant=tenant).count(),
|
'virtualmachine_count': VirtualMachine.objects.filter(tenant=tenant).count(),
|
||||||
|
@ -5,9 +5,10 @@ import sys
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import ProgrammingError
|
from django.db import ProgrammingError
|
||||||
from django.http import Http404, HttpResponseRedirect
|
from django.http import Http404, HttpResponseRedirect
|
||||||
from django.shortcuts import render
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
|
from .views import server_error
|
||||||
|
|
||||||
BASE_PATH = getattr(settings, 'BASE_PATH', False)
|
BASE_PATH = getattr(settings, 'BASE_PATH', False)
|
||||||
LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
|
LOGIN_REQUIRED = getattr(settings, 'LOGIN_REQUIRED', False)
|
||||||
|
|
||||||
@ -65,23 +66,19 @@ class ExceptionHandlingMiddleware(object):
|
|||||||
if isinstance(exception, Http404):
|
if isinstance(exception, Http404):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine the type of exception
|
# Determine the type of exception. If it's a common issue, return a custom error page with instructions.
|
||||||
|
custom_template = None
|
||||||
if isinstance(exception, ProgrammingError):
|
if isinstance(exception, ProgrammingError):
|
||||||
template_name = 'exceptions/programming_error.html'
|
custom_template = 'exceptions/programming_error.html'
|
||||||
elif isinstance(exception, ImportError):
|
elif isinstance(exception, ImportError):
|
||||||
template_name = 'exceptions/import_error.html'
|
custom_template = 'exceptions/import_error.html'
|
||||||
elif (
|
elif (
|
||||||
sys.version_info[0] >= 3 and isinstance(exception, PermissionError)
|
sys.version_info[0] >= 3 and isinstance(exception, PermissionError)
|
||||||
) or (
|
) or (
|
||||||
isinstance(exception, OSError) and exception.errno == 13
|
isinstance(exception, OSError) and exception.errno == 13
|
||||||
):
|
):
|
||||||
template_name = 'exceptions/permission_error.html'
|
custom_template = 'exceptions/permission_error.html'
|
||||||
else:
|
|
||||||
template_name = '500.html'
|
|
||||||
|
|
||||||
# Return an error message
|
# Return a custom error message, or fall back to Django's default 500 error handling
|
||||||
type_, error, traceback = sys.exc_info()
|
if custom_template:
|
||||||
return render(request, template_name, {
|
return server_error(request, template_name=custom_template)
|
||||||
'exception': str(type_),
|
|
||||||
'error': error,
|
|
||||||
}, status=500)
|
|
||||||
|
@ -2,6 +2,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from copy import deepcopy
|
from copy import deepcopy
|
||||||
|
import sys
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
@ -10,12 +11,16 @@ from django.core.exceptions import ValidationError
|
|||||||
from django.db import transaction, IntegrityError
|
from django.db import transaction, IntegrityError
|
||||||
from django.db.models import ProtectedError
|
from django.db.models import ProtectedError
|
||||||
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
from django.forms import CharField, Form, ModelMultipleChoiceField, MultipleHiddenInput, Textarea
|
||||||
|
from django.http import HttpResponseServerError
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.template.exceptions import TemplateSyntaxError
|
from django.template import loader
|
||||||
|
from django.template.exceptions import TemplateDoesNotExist, TemplateSyntaxError
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.http import is_safe_url
|
from django.utils.http import is_safe_url
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.views.decorators.csrf import requires_csrf_token
|
||||||
|
from django.views.defaults import ERROR_500_TEMPLATE_NAME
|
||||||
from django.views.generic import View
|
from django.views.generic import View
|
||||||
from django_tables2 import RequestConfig
|
from django_tables2 import RequestConfig
|
||||||
|
|
||||||
@ -858,3 +863,20 @@ class BulkComponentCreateView(View):
|
|||||||
'table': table,
|
'table': table,
|
||||||
'return_url': reverse(self.default_return_url),
|
'return_url': reverse(self.default_return_url),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@requires_csrf_token
|
||||||
|
def server_error(request, template_name=ERROR_500_TEMPLATE_NAME):
|
||||||
|
"""
|
||||||
|
Custom 500 handler to provide additional context when rendering 500.html.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
template = loader.get_template(template_name)
|
||||||
|
except TemplateDoesNotExist:
|
||||||
|
return HttpResponseServerError('<h1>Server Error (500)</h1>', content_type='text/html')
|
||||||
|
type_, error, traceback = sys.exc_info()
|
||||||
|
|
||||||
|
return HttpResponseServerError(template.render({
|
||||||
|
'exception': str(type_),
|
||||||
|
'error': error,
|
||||||
|
}))
|
||||||
|
Loading…
Reference in New Issue
Block a user