diff --git a/.github/stale.yml b/.github/stale.yml index 43401de8a..fdfb1d590 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -4,19 +4,19 @@ only: issues # Number of days of inactivity before an issue becomes stale -daysUntilStale: 14 +daysUntilStale: 45 # Number of days of inactivity before a stale issue is closed -daysUntilClose: 7 +daysUntilClose: 15 # Issues with these labels will never be considered stale exemptLabels: - "status: accepted" - - "status: gathering feedback" - "status: blocked" + - "status: needs milestone" # Label to use when marking an issue as stale -staleLabel: wontfix +staleLabel: "pending closure" # Comment to post when marking an issue as stale. Set to `false` to disable markComment: > diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cceea27b6..4f448f5ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -99,6 +99,10 @@ help prevent wasting time on something that might we might not be able to implement. When suggesting a new feature, also make sure it won't conflict with any work that's already in progress. +* Once you've opened or identified an issue you'd like to work on, ask that it +be assigned to you so that others are aware it's being worked on. A maintainer +will then mark the issue as "accepted." + * Any pull request which does _not_ relate to an accepted issue will be closed. * All major new functionality must include relevant tests where applicable. @@ -132,18 +136,17 @@ accumulating a large backlog of work. The core maintainers group has chosen to make use of GitHub's [Stale bot](https://github.com/apps/stale) to aid in issue management. -* Issues will be marked as stale after 14 days of no activity. -* Then after 7 more days of inactivity, the issue will be closed. +* Issues will be marked as stale after 45 days of no activity. +* Then after 15 more days of inactivity, the issue will be closed. * Any issue bearing one of the following labels will be exempt from all Stale bot actions: * `status: accepted` - * `status: gathering feedback` * `status: blocked` + * `status: needs milestone` -It is natural that some new issues get more attention than others. Often this -is a metric of an issues's overall value to the project. In other cases in -which issues merely get lost in the shuffle, notifications from Stale bot can -bring renewed attention to potentially meaningful issues. +It is natural that some new issues get more attention than others. Stale bot +helps bring renewed attention to potentially valuable issues that may have been +overlooked. ## Maintainer Guidance diff --git a/docs/additional-features/custom-scripts.md b/docs/additional-features/custom-scripts.md index edfc06d26..f06856502 100644 --- a/docs/additional-features/custom-scripts.md +++ b/docs/additional-features/custom-scripts.md @@ -39,9 +39,11 @@ The `run()` method should accept two arguments: * `commit` - A boolean indicating whether database changes will be committed. !!! note - The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however moving forward scripts should accept both arguments. This backward compatibility will be removed in v2.10. + The `commit` argument was introduced in NetBox v2.7.8. Backward compatibility is maintained for scripts which accept only the `data` argument, however beginning with v2.10 NetBox will require the `run()` method of every script to accept both arguments. (Either argument may still be ignored within the method.) -Returning output from your script is optional. Any raw output generated by the script will be displayed under the "output" tab in the UI. +Defining script variables is optional: You may create a script with only a `run()` method if no user input is needed. + +Any output generated by the script during its execution will be displayed under the "output" tab in the UI. ## Module Attributes diff --git a/docs/installation/upgrading.md b/docs/installation/upgrading.md index c34fef954..807b9b1e6 100644 --- a/docs/installation/upgrading.md +++ b/docs/installation/upgrading.md @@ -30,6 +30,12 @@ Copy the 'configuration.py' you created when first installing to the new version # cp netbox-X.Y.Z/netbox/netbox/configuration.py netbox/netbox/netbox/configuration.py ``` +Copy your local requirements file if used: + +```no-highlight +# cp netbox-X.Y.Z/local_requirements.txt netbox/local_requirements.txt +``` + Also copy the LDAP configuration if using LDAP: ```no-highlight diff --git a/docs/plugins/development.md b/docs/plugins/development.md index ad7eef310..b704ad7fc 100644 --- a/docs/plugins/development.md +++ b/docs/plugins/development.md @@ -110,6 +110,8 @@ NetBox looks for the `config` variable within a plugin's `__init__.py` to load i | `template_extensions` | The dotted path to the list of template extension classes (default: `template_content.template_extensions`) | | `menu_items` | The dotted path to the list of menu items provided by the plugin (default: `navigation.menu_items`) | +All required settings must be configured by the user. If a configuration parameter is listed in both `required_settings` and `default_settings`, the default setting will be ignored. + ### Install the Plugin for Development To ease development, it is recommended to go ahead and install the plugin at this point using setuptools' `develop` mode. This will create symbolic links within your Python environment to the plugin development directory. Call `setup.py` from the plugin's root directory with the `develop` argument (instead of `install`): diff --git a/docs/release-notes/version-2.8.md b/docs/release-notes/version-2.8.md index 774de491a..af758f928 100644 --- a/docs/release-notes/version-2.8.md +++ b/docs/release-notes/version-2.8.md @@ -1,13 +1,25 @@ # NetBox v2.8 -## v2.8.9 (FUTURE) +## v2.8.9 (2020-08-04) + +### Enhancements + +* [#4898](https://github.com/netbox-community/netbox/issues/4898) - Add MAC address search field to interfaces list +* [#4899](https://github.com/netbox-community/netbox/issues/4899) - Add MAC address column to interfaces table ### Bug Fixes +* [#4455](https://github.com/netbox-community/netbox/issues/4455) - Fix ordering of prefixes beneath aggregate when available space is hidden * [#4875](https://github.com/netbox-community/netbox/issues/4875) - Fix documentation for image attachments * [#4876](https://github.com/netbox-community/netbox/issues/4876) - Fix labels for sites in staging or decommissioning status -* [#4880](https://github.com/netbox-community/netbox/issues/4880) - Fix remove tagged vlans if not assigned in bulk interface editting +* [#4880](https://github.com/netbox-community/netbox/issues/4880) - Fix removal of tagged VLANs if not assigned in bulk interface editing * [#4887](https://github.com/netbox-community/netbox/issues/4887) - Don't disable NAPALM tabs when device has no primary IP +* [#4894](https://github.com/netbox-community/netbox/issues/4894) - Fix display of device/VM counts on platforms list +* [#4895](https://github.com/netbox-community/netbox/issues/4895) - Force UTF-8 encoding when embedding model documentation +* [#4910](https://github.com/netbox-community/netbox/issues/4910) - Unpin redis dependency to fix exception in RQ worker +* [#4926](https://github.com/netbox-community/netbox/issues/4926) - Fix ordering of VM interfaces in REST API endpoint +* [#4927](https://github.com/netbox-community/netbox/issues/4927) - Fix validation error when updating an existing secret +* [#4929](https://github.com/netbox-community/netbox/issues/4929) - Correct log message when creating a new object --- diff --git a/docs/release-notes/version-2.9.md b/docs/release-notes/version-2.9.md index 16e800e70..d564978bd 100644 --- a/docs/release-notes/version-2.9.md +++ b/docs/release-notes/version-2.9.md @@ -1,11 +1,31 @@ # NetBox v2.9 -## v2.9.0 (FUTURE) +## v2.9-beta2 (FUTURE) + +### Enhancements + +* [#4919](https://github.com/netbox-community/netbox/issues/4919) - Allow adding/changing assigned permissions within group and user admin views +* [#4940](https://github.com/netbox-community/netbox/issues/4940) - Add an `occupied` field to rack unit representations for rack elevation views +* [#4945](https://github.com/netbox-community/netbox/issues/4945) - Add a user-friendly 403 error page ### Bug Fixes * [#4905](https://github.com/netbox-community/netbox/issues/4905) - Fix front port count on device type view * [#4912](https://github.com/netbox-community/netbox/issues/4912) - Fix image attachment API endpoint +* [#4914](https://github.com/netbox-community/netbox/issues/4914) - Fix toggling cable status under device view +* [#4921](https://github.com/netbox-community/netbox/issues/4921) - Render non-viewable devices as unavailable space in rack elevations +* [#4930](https://github.com/netbox-community/netbox/issues/4930) - Replicate label values when instantiating device type components +* [#4931](https://github.com/netbox-community/netbox/issues/4931) - Fix DoesNotExist exception when deleting devices +* [#4938](https://github.com/netbox-community/netbox/issues/4938) - Show add, import buttons on virtual chassis list view +* [#4939](https://github.com/netbox-community/netbox/issues/4939) - Fix linking to LAG interfaces on other VC members +* [#4950](https://github.com/netbox-community/netbox/issues/4950) - Include inventory item label in API serializer, UI view +* [#4952](https://github.com/netbox-community/netbox/issues/4952) - Default to VM tab when creating/editing an IP address for a VM + +### Other Changes + +* [#4940](https://github.com/netbox-community/netbox/issues/4940) - Add an `occupied` field to rack unit representations for rack elevation views +* [#4942](https://github.com/netbox-community/netbox/issues/4942) - Make ObjectPermission's `name` field required +* [#4943](https://github.com/netbox-community/netbox/issues/4943) - Add a `description` field to ObjectPermission --- @@ -76,6 +96,7 @@ When running a report or custom script, its execution is now queued for backgrou * dcim.PowerPortTemplate: Added `description` and `label` fields * dcim.PowerOutlet: Added `label` field * dcim.PowerOutletTemplate: Added `description` and `label` fields +* dcim.Rack: Added an `occupied` field to rack unit representations for rack elevation views * dcim.RackGroup: Added a `_depth` attribute indicating an object's position in the tree. * dcim.RackReservation: Added `tags` field * dcim.RearPort: Added `label` field diff --git a/docs/rest-api/overview.md b/docs/rest-api/overview.md index 6c1f59daa..bdf76f444 100644 --- a/docs/rest-api/overview.md +++ b/docs/rest-api/overview.md @@ -267,6 +267,10 @@ GET /api/ipam/prefixes/13980/?brief=1 The brief format is supported for both lists and individual objects. +### Excluding Config Contexts + +When retrieving devices and virtual machines via the REST API, each will included its rendered [configuration context data](../models/extras/configcontext/) by default. Users with large amounts of context data will likely observe suboptimal performance when returning multiple objects, particularly with very high page sizes. To combat this, context data may be excluded from the response data by attaching the query parameter `?exclude=config_context` to the request. This parameter works for both list and detail views. + ## Pagination API responses which contain a list of many objects will be paginated for efficiency. The root JSON object returned by a list endpoint contains the following attributes: diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index c97298b2b..d33af5b5c 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -165,6 +165,7 @@ class RackUnitSerializer(serializers.Serializer): name = serializers.CharField(read_only=True) face = ChoiceField(choices=DeviceFaceChoices, read_only=True) device = NestedDeviceSerializer(read_only=True) + occupied = serializers.BooleanField(read_only=True) class RackReservationSerializer(TaggedObjectSerializer, ValidatedModelSerializer): @@ -639,8 +640,8 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class Meta: model = InventoryItem fields = [ - 'id', 'url', 'device', 'parent', 'name', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'discovered', - 'description', 'tags', + 'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', + 'discovered', 'description', 'tags', ] diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 038907b97..86d72c16c 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -176,6 +176,7 @@ class RackViewSet(CustomFieldModelViewSet): # Render and return the elevation as an SVG drawing with the correct content type drawing = rack.get_elevation_svg( face=data['face'], + user=request.user, unit_width=data['unit_width'], unit_height=data['unit_height'], legend_width=data['legend_width'], @@ -188,6 +189,7 @@ class RackViewSet(CustomFieldModelViewSet): # Return a JSON representation of the rack units in the elevation elevation = rack.get_rack_units( face=data['face'], + user=request.user, exclude=data['exclude'], expand_devices=data['expand_devices'] ) diff --git a/netbox/dcim/elevations.py b/netbox/dcim/elevations.py index ea780b2d9..cef95a7b6 100644 --- a/netbox/dcim/elevations.py +++ b/netbox/dcim/elevations.py @@ -14,10 +14,11 @@ class RackElevationSVG: Use this class to render a rack elevation as an SVG image. :param rack: A NetBox Rack instance + :param user: User instance. If specified, only devices viewable by this user will be fully displayed. :param include_images: If true, the SVG document will embed front/rear device face images, where available :param base_url: Base URL for links within the SVG document. If none, links will be relative. """ - def __init__(self, rack, include_images=True, base_url=None): + def __init__(self, rack, user=None, include_images=True, base_url=None): self.rack = rack self.include_images = include_images if base_url is not None: @@ -25,7 +26,14 @@ class RackElevationSVG: else: self.base_url = '' - def _get_device_description(self, device): + # Determine the subset of devices within this rack that are viewable by the user, if any + permitted_devices = self.rack.devices + if user is not None: + permitted_devices = permitted_devices.restrict(user, 'view') + self.permitted_device_ids = permitted_devices.values_list('pk', flat=True) + + @staticmethod + def _get_device_description(device): return '{} ({}) — {} ({}U) {} {}'.format( device.name, device.device_role, @@ -174,10 +182,13 @@ class RackElevationSVG: text_cordinates = (x_offset + (unit_width / 2), y_offset + end_y / 2) # Draw the device - if device and device.face == face: + if device and device.face == face and device.pk in self.permitted_device_ids: self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates) - elif device and device.device_type.is_full_depth: + elif device and device.device_type.is_full_depth and device.pk in self.permitted_device_ids: self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates) + elif device: + # Devices which the user does not have permission to view are rendered only as unavailable space + drawing.add(drawing.rect(start_cordinates, end_cordinates, class_='blocked')) else: # Draw shallow devices, reservations, or empty units class_ = 'slot' diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 47b19f267..febeeaa12 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -2703,6 +2703,10 @@ class InterfaceFilterForm(DeviceComponentFilterForm): choices=BOOLEAN_WITH_BLANK_CHOICES ) ) + mac_address = forms.CharField( + required=False, + label='MAC address' + ) tag = TagFilterField(model) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index 3deea8140..2803bb8de 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -656,12 +656,14 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def get_status_class(self): return self.STATUS_CLASS_MAP.get(self.status) - def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True): + def get_rack_units(self, user=None, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) + :param user: User instance to be used for evaluating device view permissions. If None, all devices + will be included. :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack :param expand_devices: When True, all units that a device occupies will be listed with each containing a reference to the device. When False, only the bottom most unit for a device is included and that unit @@ -670,10 +672,18 @@ class Rack(ChangeLoggedModel, CustomFieldModel): elevation = OrderedDict() for u in self.units: - elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None} + elevation[u] = { + 'id': u, + 'name': f'U{u}', + 'face': face, + 'device': None, + 'occupied': False + } # Add devices to rack units list if self.pk: + + # Retrieve all devices installed within the rack queryset = Device.objects.prefetch_related( 'device_type', 'device_type__manufacturer', @@ -689,12 +699,22 @@ class Rack(ChangeLoggedModel, CustomFieldModel): ).filter( Q(face=face) | Q(device_type__is_full_depth=True) ) + + # Determine which devices the user has permission to view + permitted_device_ids = [] + if user is not None: + permitted_device_ids = self.devices.restrict(user, 'view').values_list('pk', flat=True) + for device in queryset: if expand_devices: for u in range(device.position, device.position + device.device_type.u_height): - elevation[u]['device'] = device + if user is None or device.pk in permitted_device_ids: + elevation[u]['device'] = device + elevation[u]['occupied'] = True else: - elevation[device.position]['device'] = device + if user is None or device.pk in permitted_device_ids: + elevation[device.position]['device'] = device + elevation[device.position]['occupied'] = True elevation[device.position]['height'] = device.device_type.u_height for u in range(device.position + 1, device.position + device.device_type.u_height): elevation.pop(u, None) @@ -750,6 +770,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): def get_elevation_svg( self, face=DeviceFaceChoices.FACE_FRONT, + user=None, unit_width=settings.RACK_ELEVATION_DEFAULT_UNIT_WIDTH, unit_height=settings.RACK_ELEVATION_DEFAULT_UNIT_HEIGHT, legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT, @@ -760,6 +781,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): Return an SVG of the rack elevation :param face: Enum of [front, rear] representing the desired side of the rack elevation to render + :param user: User instance to be used for evaluating device view permissions. If None, all devices + will be included. :param unit_width: Width in pixels for the rendered drawing :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total height of the elevation @@ -767,7 +790,7 @@ class Rack(ChangeLoggedModel, CustomFieldModel): :param include_images: Embed front/rear device images where available :param base_url: Base URL for links and images. If none, URLs will be relative. """ - elevation = RackElevationSVG(self, include_images=include_images, base_url=base_url) + elevation = RackElevationSVG(self, user=user, include_images=include_images, base_url=base_url) return elevation.render(face, unit_width, unit_height, legend_width) diff --git a/netbox/dcim/models/device_component_templates.py b/netbox/dcim/models/device_component_templates.py index 7e96a8016..492fe3762 100644 --- a/netbox/dcim/models/device_component_templates.py +++ b/netbox/dcim/models/device_component_templates.py @@ -100,6 +100,7 @@ class ConsolePortTemplate(ComponentTemplateModel): return ConsolePort( device=device, name=self.name, + label=self.label, type=self.type ) @@ -122,6 +123,7 @@ class ConsoleServerPortTemplate(ComponentTemplateModel): return ConsoleServerPort( device=device, name=self.name, + label=self.label, type=self.type ) @@ -156,6 +158,7 @@ class PowerPortTemplate(ComponentTemplateModel): return PowerPort( device=device, name=self.name, + label=self.label, type=self.type, maximum_draw=self.maximum_draw, allocated_draw=self.allocated_draw @@ -205,6 +208,7 @@ class PowerOutletTemplate(ComponentTemplateModel): return PowerOutlet( device=device, name=self.name, + label=self.label, type=self.type, power_port=power_port, feed_leg=self.feed_leg @@ -239,6 +243,7 @@ class InterfaceTemplate(ComponentTemplateModel): return Interface( device=device, name=self.name, + label=self.label, type=self.type, mgmt_only=self.mgmt_only ) @@ -293,6 +298,7 @@ class FrontPortTemplate(ComponentTemplateModel): return FrontPort( device=device, name=self.name, + label=self.label, type=self.type, rear_port=rear_port, rear_port_position=self.rear_port_position @@ -320,6 +326,7 @@ class RearPortTemplate(ComponentTemplateModel): return RearPort( device=device, name=self.name, + label=self.label, type=self.type, positions=self.positions ) @@ -336,5 +343,6 @@ class DeviceBayTemplate(ComponentTemplateModel): def instantiate(self, device): return DeviceBay( device=device, - name=self.name + name=self.name, + label=self.label ) diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 6764f8bcf..4779b867a 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -71,11 +71,16 @@ class ComponentModel(models.Model): def to_objectchange(self, action): # Annotate the parent Device + try: + device = self.device + except ObjectDoesNotExist: + # The parent Device has already been deleted + device = None return ObjectChange( changed_object=self, object_repr=str(self), action=action, - related_object=self.device, + related_object=device, object_data=serialize_object(self) ) diff --git a/netbox/dcim/tables.py b/netbox/dcim/tables.py index 6a6b680b8..e48eaedba 100644 --- a/netbox/dcim/tables.py +++ b/netbox/dcim/tables.py @@ -56,10 +56,49 @@ DEVICE_COUNT = """ {{ value|default:0 }} """ -VM_COUNT = """ +RACKRESERVATION_ACTIONS = """ + + + +{% if perms.dcim.change_rackreservation %} + +{% endif %} +""" + +MANUFACTURER_ACTIONS = """ + + + +{% if perms.dcim.change_manufacturer %} + +{% endif %} +""" + +DEVICEROLE_DEVICE_COUNT = """ +{{ value|default:0 }} +""" + +DEVICEROLE_VM_COUNT = """ {{ value|default:0 }} """ +DEVICEROLE_ACTIONS = """ + + + +{% if perms.dcim.change_devicerole %} + +{% endif %} +""" + +PLATFORM_DEVICE_COUNT = """ +{{ value|default:0 }} +""" + +PLATFORM_VM_COUNT = """ +{{ value|default:0 }} +""" + STATUS_LABEL = """ {{ record.get_status_display }} """ @@ -495,11 +534,11 @@ class DeviceBayTemplateTable(ComponentTemplateTable): class DeviceRoleTable(BaseTable): pk = ToggleColumn() device_count = tables.TemplateColumn( - template_code=DEVICE_COUNT, + template_code=DEVICEROLE_DEVICE_COUNT, verbose_name='Devices' ) vm_count = tables.TemplateColumn( - template_code=VM_COUNT, + template_code=DEVICEROLE_VM_COUNT, verbose_name='VMs' ) color = tables.TemplateColumn( @@ -522,11 +561,11 @@ class DeviceRoleTable(BaseTable): class PlatformTable(BaseTable): pk = ToggleColumn() device_count = tables.TemplateColumn( - template_code=DEVICE_COUNT, + template_code=PLATFORM_DEVICE_COUNT, verbose_name='Devices' ) vm_count = tables.TemplateColumn( - template_code=VM_COUNT, + template_code=PLATFORM_VM_COUNT, verbose_name='VMs' ) actions = ButtonsColumn(Platform, pk_field='slug') @@ -718,8 +757,8 @@ class InterfaceTable(DeviceComponentTable, BaseInterfaceTable): class Meta(DeviceComponentTable.Meta): model = Interface fields = ( - 'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'description', 'cable', - 'ip_addresses', 'untagged_vlan', 'tagged_vlans', + 'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address', + 'description', 'cable', 'ip_addresses', 'untagged_vlan', 'tagged_vlans', ) default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description') diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index 2cee5a413..f3e4b204d 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -953,8 +953,8 @@ class DeviceRoleBulkDeleteView(BulkDeleteView): class PlatformListView(ObjectListView): queryset = Platform.objects.annotate( - device_count=get_subquery(Device, 'device_role'), - vm_count=get_subquery(VirtualMachine, 'role') + device_count=get_subquery(Device, 'platform'), + vm_count=get_subquery(VirtualMachine, 'platform') ) table = tables.PlatformTable @@ -2185,7 +2185,6 @@ class VirtualChassisListView(ObjectListView): table = tables.VirtualChassisTable filterset = filters.VirtualChassisFilterSet filterset_form = forms.VirtualChassisFilterForm - action_buttons = ('export',) class VirtualChassisView(ObjectView): diff --git a/netbox/extras/admin.py b/netbox/extras/admin.py index 629edf5ce..a198d03d5 100644 --- a/netbox/extras/admin.py +++ b/netbox/extras/admin.py @@ -3,7 +3,6 @@ from django.contrib import admin from utilities.forms import LaxURLField from .models import CustomField, CustomFieldChoice, CustomLink, Graph, ExportTemplate, JobResult, Webhook -from .reports import get_report def order_content_types(field): @@ -160,6 +159,10 @@ class GraphForm(forms.ModelForm): class Meta: model = Graph exclude = () + help_texts = { + 'template_language': "Jinja2 is strongly recommended for " + "new graphs." + } widgets = { 'source': forms.Textarea, 'link': forms.Textarea, @@ -195,6 +198,11 @@ class ExportTemplateForm(forms.ModelForm): class Meta: model = ExportTemplate exclude = [] + help_texts = { + 'template_language': "Warning: Support for Django templating will be dropped in NetBox " + "v2.10. Jinja2 is strongly " + "recommended." + } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/netbox/extras/choices.py b/netbox/extras/choices.py index f911f4f9d..b14748135 100644 --- a/netbox/extras/choices.py +++ b/netbox/extras/choices.py @@ -85,12 +85,12 @@ class ObjectChangeActionChoices(ChoiceSet): class TemplateLanguageChoices(ChoiceSet): - LANGUAGE_DJANGO = 'django' LANGUAGE_JINJA2 = 'jinja2' + LANGUAGE_DJANGO = 'django' CHOICES = ( - (LANGUAGE_DJANGO, 'Django'), (LANGUAGE_JINJA2, 'Jinja2'), + (LANGUAGE_DJANGO, 'Django (Legacy)'), ) diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index 5955b6252..c3957d4be 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -4,6 +4,7 @@ import logging import os import pkgutil import traceback +import warnings from collections import OrderedDict import yaml @@ -405,12 +406,16 @@ def run_script(data, request, commit=True, *args, **kwargs): # Add the current request as a property of the script script.request = request + # TODO: Drop backward-compatibility for absent 'commit' argument in v2.10 # Determine whether the script accepts a 'commit' argument (this was introduced in v2.7.8) kwargs = { 'data': data } if 'commit' in inspect.signature(script.run).parameters: kwargs['commit'] = commit + else: + warnings.warn(f"The run() method of script {script} should support a 'commit' argument. This will be required " + f"beginning with NetBox v2.10.") try: with transaction.atomic(): diff --git a/netbox/ipam/views.py b/netbox/ipam/views.py index 6d0411866..ec067e705 100644 --- a/netbox/ipam/views.py +++ b/netbox/ipam/views.py @@ -219,6 +219,8 @@ class AggregateView(ObjectView): prefix__net_contained_or_equal=str(aggregate.prefix) ).prefetch_related( 'site', 'role' + ).order_by( + 'prefix' ).annotate_depth( limit=0 ) diff --git a/netbox/netbox/settings.py b/netbox/netbox/settings.py index 200207672..a46709053 100644 --- a/netbox/netbox/settings.py +++ b/netbox/netbox/settings.py @@ -16,7 +16,7 @@ from django.core.validators import URLValidator # Environment setup # -VERSION = '2.9-beta1' +VERSION = '2.9-beta2' # Hostname HOSTNAME = platform.node() diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py index f62c72293..e59819246 100644 --- a/netbox/secrets/forms.py +++ b/netbox/secrets/forms.py @@ -121,7 +121,7 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm): device=self.cleaned_data['device'], role=self.cleaned_data['role'], name=self.cleaned_data['name'] - ).exists(): + ).exclude(pk=self.instance.pk).exists(): raise forms.ValidationError( "Each secret assigned to a device must have a unique combination of role and name" ) diff --git a/netbox/templates/403.html b/netbox/templates/403.html new file mode 100644 index 000000000..6a114301f --- /dev/null +++ b/netbox/templates/403.html @@ -0,0 +1,9 @@ +{% extends '40x.html' %} + +{% block title %}Access Denied{% endblock %} + +{% block icon %}{% endblock %} + +{% block message %} + You do not have permission to access this page. +{% endblock %} diff --git a/netbox/templates/404.html b/netbox/templates/404.html index f2fe6b430..22c17fed4 100644 --- a/netbox/templates/404.html +++ b/netbox/templates/404.html @@ -1,19 +1,9 @@ -{% extends 'base.html' %} +{% extends '40x.html' %} -{% block content %} -
Type | -{{ cable.get_type_display }} | +{{ cable.get_type_display|placeholder }} | |
Status | -{{ cable.get_status_display }} | ++ {{ cable.get_status_display }} + | |
Label | diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index 2f82ce5e8..ff3cc71f8 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -975,7 +975,7 @@ function toggleConnection(elem) { xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}"); }, data: { - 'status': 'False' + 'status': 'planned' }, context: this, success: function() { @@ -994,7 +994,7 @@ function toggleConnection(elem) { xhr.setRequestHeader("X-CSRFToken", "{{ csrf_token }}"); }, data: { - 'status': 'True' + 'status': 'connected' }, context: this, success: function() { diff --git a/netbox/templates/dcim/inc/interface.html b/netbox/templates/dcim/inc/interface.html index 75869db52..3d7dd4c3d 100644 --- a/netbox/templates/dcim/inc/interface.html +++ b/netbox/templates/dcim/inc/interface.html @@ -22,7 +22,7 @@ {# LAG #}{% if iface.lag %} - {{ iface.lag }} + {{ iface.lag }} {% endif %} | diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index 6e12d9f33..54f49d5f2 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -30,6 +30,10 @@Name | {{ instance.name }} |
Label | +{{ instance.label|placeholder }} | +||
Manufacturer |
diff --git a/netbox/templates/ipam/ipaddress_edit.html b/netbox/templates/ipam/ipaddress_edit.html
index 4e2706daf..0748bc5ca 100644
--- a/netbox/templates/ipam/ipaddress_edit.html
+++ b/netbox/templates/ipam/ipaddress_edit.html
@@ -33,7 +33,7 @@
Interface Assignment
- {% with vm_tab_active=obj.vminterface.exists %}
+ {% with vm_tab_active=form.initial.vminterface %}
|