mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
Merge branch 'develop' into develop-2.4
This commit is contained in:
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
|
||||||
|
@ -260,7 +260,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:
|
||||||
@ -274,16 +274,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)
|
||||||
optional_args = settings.NAPALM_ARGS.copy()
|
optional_args = settings.NAPALM_ARGS.copy()
|
||||||
@ -298,12 +290,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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -521,7 +521,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()
|
||||||
|
|
||||||
|
@ -1085,9 +1085,6 @@ class DeviceRole(ChangeLoggedModel):
|
|||||||
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)
|
||||||
|
@ -432,7 +432,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'),
|
||||||
|
@ -193,8 +193,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
|
||||||
|
@ -157,7 +157,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):
|
||||||
|
@ -219,7 +219,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>
|
||||||
|
@ -77,10 +77,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 %}
|
||||||
|
@ -99,13 +99,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 %}
|
||||||
|
@ -75,14 +75,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(),
|
||||||
|
Reference in New Issue
Block a user