1
0
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:
Jeremy Stretch
2018-07-18 17:51:57 -04:00
13 changed files with 37 additions and 49 deletions

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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()

View File

@ -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,

View File

@ -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)

View File

@ -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'),

View File

@ -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

View File

@ -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):

View File

@ -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>

View File

@ -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 %}

View File

@ -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 %}

View File

@ -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(),