mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-25 18:08:38 -06:00
Merge branch 'master' into develop
This commit is contained in:
commit
ce03b62386
@ -491,6 +491,14 @@ The file path to the location where custom reports will be kept. By default, thi
|
||||
|
||||
---
|
||||
|
||||
## RQ_DEFAULT_TIMEOUT
|
||||
|
||||
Default: `300`
|
||||
|
||||
The maximum execution time of a background task (such as running a custom script), in seconds.
|
||||
|
||||
---
|
||||
|
||||
## SCRIPTS_ROOT
|
||||
|
||||
Default: `$INSTALL_ROOT/netbox/scripts/`
|
||||
|
@ -65,7 +65,6 @@ Redis is configured using a configuration setting similar to `DATABASE` and thes
|
||||
* `PORT` - TCP port of the Redis service; leave blank for default port (6379)
|
||||
* `PASSWORD` - Redis password (if set)
|
||||
* `DATABASE` - Numeric database ID
|
||||
* `DEFAULT_TIMEOUT` - Connection timeout in seconds
|
||||
* `SSL` - Use SSL connection to Redis
|
||||
|
||||
An example configuration is provided below:
|
||||
@ -77,7 +76,6 @@ REDIS = {
|
||||
'PORT': 1234,
|
||||
'PASSWORD': 'foobar',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
@ -85,7 +83,6 @@ REDIS = {
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
@ -109,6 +106,7 @@ above and the addition of two new keys.
|
||||
* `SENTINELS`: List of tuples or tuple of tuples with each inner tuple containing the name or IP address
|
||||
of the Redis server and port for each sentinel instance to connect to
|
||||
* `SENTINEL_SERVICE`: Name of the master / service to connect to
|
||||
* `SENTINEL_TIMEOUT`: Connection timeout, in seconds
|
||||
|
||||
Example:
|
||||
|
||||
@ -117,9 +115,9 @@ REDIS = {
|
||||
'tasks': {
|
||||
'SENTINELS': [('mysentinel.redis.example.com', 6379)],
|
||||
'SENTINEL_SERVICE': 'netbox',
|
||||
'SENTINEL_TIMEOUT': 10,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
@ -130,7 +128,6 @@ REDIS = {
|
||||
'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ Begin by installing all system packages required by NetBox and its dependencies.
|
||||
Before continuing with either platform, update pip (Python's package management tool) to its latest release:
|
||||
|
||||
```no-highlight
|
||||
# pip install --upgrade pip
|
||||
# pip3 install --upgrade pip
|
||||
```
|
||||
|
||||
## Download NetBox
|
||||
@ -163,7 +163,6 @@ REDIS = {
|
||||
'PORT': 6379, # Redis port
|
||||
'PASSWORD': '', # Redis password (optional)
|
||||
'DATABASE': 0, # Database ID
|
||||
'DEFAULT_TIMEOUT': 300, # Timeout (seconds)
|
||||
'SSL': False, # Use SSL (optional)
|
||||
},
|
||||
'caching': {
|
||||
@ -171,7 +170,6 @@ REDIS = {
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1, # Unique ID for second database
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,3 @@
|
||||
## Rear Port Templates
|
||||
|
||||
A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. Each rear port may have a physical type and one or more front port templates assigned to it. The number of positions associated with a rear port determines how many front ports can be assigned to it (the maximum is 64).
|
||||
A template for a rear-facing pass-through port that will be created on all instantiations of the parent device type. Each rear port may have a physical type and one or more front port templates assigned to it. The number of positions associated with a rear port determines how many front ports can be assigned to it (the maximum is 1024).
|
||||
|
@ -328,6 +328,9 @@ A `PluginMenuButton` has the following attributes:
|
||||
* `color` - One of the choices provided by `ButtonColorChoices` (optional)
|
||||
* `permissions` - A list of permissions required to display this button (optional)
|
||||
|
||||
!!! note
|
||||
Any buttons associated within a menu item will be shown only if the user has permission to view the link, regardless of what permissions are set on the buttons.
|
||||
|
||||
## Extending Core Templates
|
||||
|
||||
Plugins can inject custom content into certain areas of the detail views of applicable models. This is accomplished by subclassing `PluginTemplateExtension`, designating a particular NetBox model, and defining the desired methods to render custom content. Four methods are available:
|
||||
|
@ -1,6 +1,68 @@
|
||||
# NetBox v2.9
|
||||
|
||||
## v2.9.2 (FUTURE)
|
||||
## v2.9.4 (2020-09-23)
|
||||
|
||||
**NOTE:** This release removes support for the `DEFAULT_TIMEOUT` parameter under `REDIS` database configuration. Set `RQ_DEFAULT_TIMEOUT` as a global configuration parameter instead.
|
||||
|
||||
**NOTE:** Any permissions referencing the legacy ReportResult model (e.g. `extras.view_reportresult`) should be updated to reference the Report model.
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#1755](https://github.com/netbox-community/netbox/issues/1755) - Toggle order in which rack elevations are displayed
|
||||
* [#5128](https://github.com/netbox-community/netbox/issues/5128) - Increase maximum rear port positions from 64 to 1024
|
||||
* [#5134](https://github.com/netbox-community/netbox/issues/5134) - Display full hierarchy in breadcrumbs for sites/racks
|
||||
* [#5149](https://github.com/netbox-community/netbox/issues/5149) - Add rack group field to device edit form
|
||||
* [#5164](https://github.com/netbox-community/netbox/issues/5164) - Show total rack count per rack group under site view
|
||||
* [#5171](https://github.com/netbox-community/netbox/issues/5171) - Introduce the `RQ_DEFAULT_TIMEOUT` configuration parameter
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5050](https://github.com/netbox-community/netbox/issues/5050) - Fix potential failure on `0016_replicate_interfaces` schema migration from old release
|
||||
* [#5066](https://github.com/netbox-community/netbox/issues/5066) - Update `view_reportresult` to `view_report` permission
|
||||
* [#5075](https://github.com/netbox-community/netbox/issues/5075) - Include a VLAN membership view for VM interfaces
|
||||
* [#5105](https://github.com/netbox-community/netbox/issues/5105) - Validation should fail when reassigning a primary IP from device to VM
|
||||
* [#5109](https://github.com/netbox-community/netbox/issues/5109) - Fix representation of custom choice field values for webhook data
|
||||
* [#5108](https://github.com/netbox-community/netbox/issues/5108) - Fix execution of reports via CLI
|
||||
* [#5111](https://github.com/netbox-community/netbox/issues/5111) - Allow use of tuples when specifying ObjectVar `query_params`
|
||||
* [#5118](https://github.com/netbox-community/netbox/issues/5118) - Specifying an empty list of tags should clear assigned tags (REST API)
|
||||
* [#5133](https://github.com/netbox-community/netbox/issues/5133) - Fix disassociation of an IP address from a VM interface
|
||||
* [#5136](https://github.com/netbox-community/netbox/issues/5136) - Fix exception when bulk editing interface 802.1Q mode
|
||||
* [#5156](https://github.com/netbox-community/netbox/issues/5156) - Add missing "add" button to rack reservations list
|
||||
* [#5167](https://github.com/netbox-community/netbox/issues/5167) - Support filtering ObjectChanges by multiple users
|
||||
|
||||
---
|
||||
|
||||
## v2.9.3 (2020-09-04)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#4977](https://github.com/netbox-community/netbox/issues/4977) - Redirect authenticated users from login view
|
||||
* [#5048](https://github.com/netbox-community/netbox/issues/5048) - Show the device/VM name when editing a component
|
||||
* [#5072](https://github.com/netbox-community/netbox/issues/5072) - Add REST API filters for image attachments
|
||||
* [#5080](https://github.com/netbox-community/netbox/issues/5080) - Add 8P6C, 8P4C, 8P2C port types
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* [#5046](https://github.com/netbox-community/netbox/issues/5046) - Disabled plugin menu items are no longer clickable
|
||||
* [#5063](https://github.com/netbox-community/netbox/issues/5063) - Fix "add device" link in rack elevations for opposite side of half-depth devices
|
||||
* [#5074](https://github.com/netbox-community/netbox/issues/5074) - Fix inclusion of VC member interfaces when viewing VC master
|
||||
* [#5078](https://github.com/netbox-community/netbox/issues/5078) - Fix assignment of existing IP addresses to interfaces via web UI
|
||||
* [#5081](https://github.com/netbox-community/netbox/issues/5081) - Fix exception during webhook processing with custom select field
|
||||
* [#5085](https://github.com/netbox-community/netbox/issues/5085) - Fix ordering by assignment in IP addresses table
|
||||
* [#5087](https://github.com/netbox-community/netbox/issues/5087) - Restore label field when editing console server ports, power ports, and power outlets
|
||||
* [#5089](https://github.com/netbox-community/netbox/issues/5089) - Redirect to device view after editing component
|
||||
* [#5090](https://github.com/netbox-community/netbox/issues/5090) - Fix status display for console/power/interface connections
|
||||
* [#5091](https://github.com/netbox-community/netbox/issues/5091) - Avoid KeyError when handling invalid table preferences
|
||||
* [#5095](https://github.com/netbox-community/netbox/issues/5095) - Show assigned prefixes in VLANs list
|
||||
|
||||
---
|
||||
|
||||
## v2.9.2 (2020-08-27)
|
||||
|
||||
### Enhancements
|
||||
|
||||
* [#5055](https://github.com/netbox-community/netbox/issues/5055) - Add tags column to device/VM component list tables
|
||||
* [#5056](https://github.com/netbox-community/netbox/issues/5056) - Add interface and parent columns to IP address list
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@ -12,6 +74,10 @@
|
||||
* [#5041](https://github.com/netbox-community/netbox/issues/5041) - Fix form tabs when assigning an IP to a VM interface
|
||||
* [#5042](https://github.com/netbox-community/netbox/issues/5042) - Fix display of SLAAC label for IP addresses status
|
||||
* [#5045](https://github.com/netbox-community/netbox/issues/5045) - Allow assignment of interfaces to non-master VC peer LAG during import
|
||||
* [#5058](https://github.com/netbox-community/netbox/issues/5058) - Correct URL for front rack elevation images when using external storage
|
||||
* [#5059](https://github.com/netbox-community/netbox/issues/5059) - Fix inclusion of checkboxes for interfaces in virtual machine view
|
||||
* [#5060](https://github.com/netbox-community/netbox/issues/5060) - Fix validation when bulk-importing child devices
|
||||
* [#5061](https://github.com/netbox-community/netbox/issues/5061) - Allow adding/removing tags when bulk editing virtual machine interfaces
|
||||
|
||||
---
|
||||
|
||||
@ -87,6 +153,7 @@ Two new REST API endpoints have been added to facilitate the retrieval and manip
|
||||
* If using NetBox's built-in remote authentication backend, update `REMOTE_AUTH_BACKEND` to `'netbox.authentication.RemoteUserBackend'`, as the authentication class has moved.
|
||||
* If using LDAP authentication, set `REMOTE_AUTH_BACKEND` to `'netbox.authentication.LDAPBackend'`. (LDAP configuration parameters in `ldap_config.py` remain unchanged.)
|
||||
* `REMOTE_AUTH_DEFAULT_PERMISSIONS` now takes a dictionary rather than a list. This is a mapping of permission names to a dictionary of constraining attributes, or `None`. For example, `['dcim.add_site', 'dcim.change_site']` would become `{'dcim.add_site': None, 'dcim.change_site': None}`.
|
||||
* Backward compatibility for the old `webhooks` Redis queue name has been dropped. Ensure that your `REDIS` configuration parameter specifies both the `tasks` and `caching` databases.
|
||||
|
||||
### REST API Changes
|
||||
|
||||
|
@ -814,6 +814,9 @@ class InterfaceModeChoices(ChoiceSet):
|
||||
class PortTypeChoices(ChoiceSet):
|
||||
|
||||
TYPE_8P8C = '8p8c'
|
||||
TYPE_8P6C = '8p6c'
|
||||
TYPE_8P4C = '8p4c'
|
||||
TYPE_8P2C = '8p2c'
|
||||
TYPE_110_PUNCH = '110-punch'
|
||||
TYPE_BNC = 'bnc'
|
||||
TYPE_MRJ21 = 'mrj21'
|
||||
@ -833,6 +836,9 @@ class PortTypeChoices(ChoiceSet):
|
||||
'Copper',
|
||||
(
|
||||
(TYPE_8P8C, '8P8C'),
|
||||
(TYPE_8P6C, '8P6C'),
|
||||
(TYPE_8P4C, '8P4C'),
|
||||
(TYPE_8P2C, '8P2C'),
|
||||
(TYPE_110_PUNCH, '110 Punch'),
|
||||
(TYPE_BNC, 'BNC'),
|
||||
(TYPE_MRJ21, 'MRJ21'),
|
||||
|
@ -18,7 +18,7 @@ RACK_ELEVATION_LEGEND_WIDTH_DEFAULT = 30
|
||||
#
|
||||
|
||||
REARPORT_POSITIONS_MIN = 1
|
||||
REARPORT_POSITIONS_MAX = 64
|
||||
REARPORT_POSITIONS_MAX = 1024
|
||||
|
||||
|
||||
#
|
||||
|
@ -94,8 +94,12 @@ class RackElevationSVG:
|
||||
|
||||
# Embed front device type image if one exists
|
||||
if self.include_images and device.device_type.front_image:
|
||||
url = '{}{}'.format(self.base_url, device.device_type.front_image.url)
|
||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
||||
image = drawing.image(
|
||||
href=device.device_type.front_image.url,
|
||||
insert=start,
|
||||
size=end,
|
||||
class_='device-image'
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
link.add(image)
|
||||
|
||||
@ -107,8 +111,12 @@ class RackElevationSVG:
|
||||
|
||||
# Embed rear device type image if one exists
|
||||
if self.include_images and device.device_type.rear_image:
|
||||
url = device.device_type.rear_image.url
|
||||
image = drawing.image(href=url, insert=start, size=end, class_='device-image')
|
||||
image = drawing.image(
|
||||
href=device.device_type.rear_image.url,
|
||||
insert=start,
|
||||
size=end,
|
||||
class_='device-image'
|
||||
)
|
||||
image.fit(scale='slice')
|
||||
drawing.add(image)
|
||||
|
||||
@ -141,7 +149,7 @@ class RackElevationSVG:
|
||||
unit_cursor = 0
|
||||
for u in elevation:
|
||||
o = other[unit_cursor]
|
||||
if not u['device'] and o['device']:
|
||||
if not u['device'] and o['device'] and o['device'].device_type.is_full_depth:
|
||||
u['device'] = o['device']
|
||||
u['height'] = 1
|
||||
unit_cursor += u.get('height', 1)
|
||||
|
@ -1680,12 +1680,21 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
|
||||
'region_id': '$region'
|
||||
}
|
||||
)
|
||||
rack_group = DynamicModelChoiceField(
|
||||
queryset=RackGroup.objects.all(),
|
||||
required=False,
|
||||
display_field='display_name',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
}
|
||||
)
|
||||
rack = DynamicModelChoiceField(
|
||||
queryset=Rack.objects.all(),
|
||||
required=False,
|
||||
display_field='display_name',
|
||||
query_params={
|
||||
'site_id': '$site'
|
||||
'site_id': '$site',
|
||||
'group_id': '$rack_group',
|
||||
}
|
||||
)
|
||||
position = forms.TypedChoiceField(
|
||||
@ -2317,7 +2326,7 @@ class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = ConsoleServerPort
|
||||
fields = [
|
||||
'device', 'name', 'type', 'description', 'tags',
|
||||
'device', 'name', 'label', 'type', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -2390,7 +2399,7 @@ class PowerPortForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerPort
|
||||
fields = [
|
||||
'device', 'name', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
||||
'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
@ -2479,7 +2488,7 @@ class PowerOutletForm(BootstrapMixin, forms.ModelForm):
|
||||
class Meta:
|
||||
model = PowerOutlet
|
||||
fields = [
|
||||
'device', 'name', 'type', 'power_port', 'feed_leg', 'description', 'tags',
|
||||
'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'tags',
|
||||
]
|
||||
widgets = {
|
||||
'device': forms.HiddenInput(),
|
||||
|
34
netbox/dcim/migrations/0116_rearport_max_positions.py
Normal file
34
netbox/dcim/migrations/0116_rearport_max_positions.py
Normal file
@ -0,0 +1,34 @@
|
||||
# Generated by Django 3.1 on 2020-09-16 16:51
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0115_rackreservation_order'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='frontport',
|
||||
name='rear_port_position',
|
||||
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='frontporttemplate',
|
||||
name='rear_port_position',
|
||||
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearport',
|
||||
name='positions',
|
||||
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='rearporttemplate',
|
||||
name='positions',
|
||||
field=models.PositiveSmallIntegerField(default=1, validators=[django.core.validators.MinValueValidator(1), django.core.validators.MaxValueValidator(1024)]),
|
||||
),
|
||||
]
|
@ -264,7 +264,10 @@ class FrontPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -315,7 +318,10 @@ class RearPortTemplate(ComponentTemplateModel):
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
@ -809,7 +809,10 @@ class FrontPort(CableTermination, ComponentModel):
|
||||
)
|
||||
rear_port_position = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
@ -864,7 +867,10 @@ class RearPort(CableTermination, ComponentModel):
|
||||
)
|
||||
positions = models.PositiveSmallIntegerField(
|
||||
default=1,
|
||||
validators=[MinValueValidator(1), MaxValueValidator(64)]
|
||||
validators=[
|
||||
MinValueValidator(REARPORT_POSITIONS_MIN),
|
||||
MaxValueValidator(REARPORT_POSITIONS_MAX)
|
||||
]
|
||||
)
|
||||
tags = TaggableManager(through=TaggedItem)
|
||||
|
||||
|
@ -633,7 +633,7 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
|
||||
# Check for a duplicate name on a device assigned to the same Site and no Tenant. This is necessary
|
||||
# because Django does not consider two NULL fields to be equal, and thus will not trigger a violation
|
||||
# of the uniqueness constraint without manual intervention.
|
||||
if self.name and self.tenant is None:
|
||||
if self.name and hasattr(self, 'site') and self.tenant is None:
|
||||
if Device.objects.exclude(pk=self.pk).filter(
|
||||
name=self.name,
|
||||
site=self.site,
|
||||
|
@ -152,6 +152,10 @@ INTERFACE_TAGGED_VLANS = """
|
||||
{% endfor %}
|
||||
"""
|
||||
|
||||
CONNECTION_STATUS = """
|
||||
<span class="label label-{% if record.connection_status %}success{% else %}danger{% endif %}">{{ record.get_connection_status_display }}</span>
|
||||
"""
|
||||
|
||||
|
||||
#
|
||||
# Regions
|
||||
@ -706,34 +710,48 @@ class DeviceComponentTable(BaseTable):
|
||||
|
||||
|
||||
class ConsolePortTable(DeviceComponentTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:consoleport_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsolePort
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class ConsoleServerPortTable(DeviceComponentTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:consoleserverport_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = ConsoleServerPort
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'cable', 'tags')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
class PowerPortTable(DeviceComponentTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:powerport_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerPort
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable')
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'description', 'maximum_draw', 'allocated_draw', 'cable', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description')
|
||||
|
||||
|
||||
class PowerOutletTable(DeviceComponentTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:poweroutlet_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = PowerOutlet
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'description', 'power_port', 'feed_leg', 'cable', 'tags')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description')
|
||||
|
||||
|
||||
@ -753,12 +771,15 @@ class BaseInterfaceTable(BaseTable):
|
||||
|
||||
|
||||
class InterfaceTable(DeviceComponentTable, BaseInterfaceTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:interface_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = Interface
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'enabled', 'type', 'mgmt_only', 'mtu', 'mode', 'mac_address',
|
||||
'description', 'cable', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||
'description', 'cable', 'tags', 'ip_addresses', 'untagged_vlan', 'tagged_vlans',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'enabled', 'type', 'description')
|
||||
|
||||
@ -767,18 +788,26 @@ class FrontPortTable(DeviceComponentTable):
|
||||
rear_port_position = tables.Column(
|
||||
verbose_name='Position'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:frontport_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = FrontPort
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable')
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description')
|
||||
|
||||
|
||||
class RearPortTable(DeviceComponentTable):
|
||||
tags = TagColumn(
|
||||
url_name='dcim:rearport_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = RearPort
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable')
|
||||
fields = ('pk', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'tags')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'type', 'description')
|
||||
|
||||
|
||||
@ -786,10 +815,13 @@ class DeviceBayTable(DeviceComponentTable):
|
||||
installed_device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='dcim:devicebay_list'
|
||||
)
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = DeviceBay
|
||||
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
fields = ('pk', 'device', 'name', 'label', 'installed_device', 'description', 'tags')
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'installed_device', 'description')
|
||||
|
||||
|
||||
@ -798,12 +830,16 @@ class InventoryItemTable(DeviceComponentTable):
|
||||
linkify=True
|
||||
)
|
||||
discovered = BooleanColumn()
|
||||
tags = TagColumn(
|
||||
url_name='dcim:inventoryitem_list'
|
||||
)
|
||||
cable = None # Override DeviceComponentTable
|
||||
|
||||
class Meta(DeviceComponentTable.Meta):
|
||||
model = InventoryItem
|
||||
fields = (
|
||||
'pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description',
|
||||
'discovered',
|
||||
'discovered', 'tags',
|
||||
)
|
||||
default_columns = ('pk', 'device', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag')
|
||||
|
||||
@ -876,15 +912,20 @@ class ConsoleConnectionTable(BaseTable):
|
||||
verbose_name='Console Server'
|
||||
)
|
||||
connected_endpoint = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Port'
|
||||
)
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Console Port'
|
||||
)
|
||||
connection_status = BooleanColumn()
|
||||
connection_status = tables.TemplateColumn(
|
||||
template_code=CONNECTION_STATUS,
|
||||
verbose_name='Status'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = ConsolePort
|
||||
@ -901,14 +942,20 @@ class PowerConnectionTable(BaseTable):
|
||||
)
|
||||
outlet = tables.Column(
|
||||
accessor=Accessor('_connected_poweroutlet'),
|
||||
linkify=True,
|
||||
verbose_name='Outlet'
|
||||
)
|
||||
device = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
name = tables.Column(
|
||||
linkify=True,
|
||||
verbose_name='Power Port'
|
||||
)
|
||||
connection_status = tables.TemplateColumn(
|
||||
template_code=CONNECTION_STATUS,
|
||||
verbose_name='Status'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = PowerPort
|
||||
@ -940,6 +987,10 @@ class InterfaceConnectionTable(BaseTable):
|
||||
args=[Accessor('_connected_interface__pk')],
|
||||
verbose_name='Interface B'
|
||||
)
|
||||
connection_status = tables.TemplateColumn(
|
||||
template_code=CONNECTION_STATUS,
|
||||
verbose_name='Status'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
|
@ -169,9 +169,13 @@ class SiteView(ObjectView):
|
||||
'circuit_count': Circuit.objects.restrict(request.user, 'view').filter(terminations__site=site).count(),
|
||||
'vm_count': VirtualMachine.objects.restrict(request.user, 'view').filter(cluster__site=site).count(),
|
||||
}
|
||||
rack_groups = RackGroup.objects.restrict(request.user, 'view').filter(site=site).annotate(
|
||||
rack_count=Count('racks')
|
||||
)
|
||||
rack_groups = RackGroup.objects.add_related_count(
|
||||
RackGroup.objects.all(),
|
||||
Rack,
|
||||
'group',
|
||||
'rack_count',
|
||||
cumulative=True
|
||||
).restrict(request.user, 'view').filter(site=site)
|
||||
show_graphs = Graph.objects.filter(type__model='site').exists()
|
||||
|
||||
return render(request, 'dcim/site.html', {
|
||||
@ -310,6 +314,11 @@ class RackElevationListView(ObjectListView):
|
||||
racks = filters.RackFilterSet(request.GET, self.queryset).qs
|
||||
total_count = racks.count()
|
||||
|
||||
# Determine ordering
|
||||
reverse = bool(request.GET.get('reverse', False))
|
||||
if reverse:
|
||||
racks = racks.reverse()
|
||||
|
||||
# Pagination
|
||||
per_page = request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
page_number = request.GET.get('page', 1)
|
||||
@ -330,6 +339,7 @@ class RackElevationListView(ObjectListView):
|
||||
'paginator': paginator,
|
||||
'page': page,
|
||||
'total_count': total_count,
|
||||
'reverse': reverse,
|
||||
'rack_face': rack_face,
|
||||
'filter_form': forms.RackElevationFilterForm(request.GET),
|
||||
})
|
||||
@ -408,7 +418,6 @@ class RackReservationListView(ObjectListView):
|
||||
filterset = filters.RackReservationFilterSet
|
||||
filterset_form = forms.RackReservationFilterForm
|
||||
table = tables.RackReservationTable
|
||||
action_buttons = ('export',)
|
||||
|
||||
|
||||
class RackReservationView(ObjectView):
|
||||
@ -1033,7 +1042,7 @@ class DeviceView(ObjectView):
|
||||
)
|
||||
|
||||
# Interfaces
|
||||
interfaces = device.vc_interfaces.restrict(request.user, 'view').filter(device=device).prefetch_related(
|
||||
interfaces = device.vc_interfaces.restrict(request.user, 'view').prefetch_related(
|
||||
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user)),
|
||||
Prefetch('member_interfaces', queryset=Interface.objects.restrict(request.user)),
|
||||
'lag', '_connected_interface__device', '_connected_circuittermination__circuit', 'cable',
|
||||
@ -1233,6 +1242,7 @@ class ConsolePortCreateView(ComponentCreateView):
|
||||
class ConsolePortEditView(ObjectEditView):
|
||||
queryset = ConsolePort.objects.all()
|
||||
model_form = forms.ConsolePortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class ConsolePortDeleteView(ObjectDeleteView):
|
||||
@ -1292,6 +1302,7 @@ class ConsoleServerPortCreateView(ComponentCreateView):
|
||||
class ConsoleServerPortEditView(ObjectEditView):
|
||||
queryset = ConsoleServerPort.objects.all()
|
||||
model_form = forms.ConsoleServerPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class ConsoleServerPortDeleteView(ObjectDeleteView):
|
||||
@ -1351,6 +1362,7 @@ class PowerPortCreateView(ComponentCreateView):
|
||||
class PowerPortEditView(ObjectEditView):
|
||||
queryset = PowerPort.objects.all()
|
||||
model_form = forms.PowerPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class PowerPortDeleteView(ObjectDeleteView):
|
||||
@ -1410,6 +1422,7 @@ class PowerOutletCreateView(ComponentCreateView):
|
||||
class PowerOutletEditView(ObjectEditView):
|
||||
queryset = PowerOutlet.objects.all()
|
||||
model_form = forms.PowerOutletForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class PowerOutletDeleteView(ObjectDeleteView):
|
||||
@ -1561,6 +1574,7 @@ class FrontPortCreateView(ComponentCreateView):
|
||||
class FrontPortEditView(ObjectEditView):
|
||||
queryset = FrontPort.objects.all()
|
||||
model_form = forms.FrontPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class FrontPortDeleteView(ObjectDeleteView):
|
||||
@ -1620,6 +1634,7 @@ class RearPortCreateView(ComponentCreateView):
|
||||
class RearPortEditView(ObjectEditView):
|
||||
queryset = RearPort.objects.all()
|
||||
model_form = forms.RearPortForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class RearPortDeleteView(ObjectDeleteView):
|
||||
@ -1679,6 +1694,7 @@ class DeviceBayCreateView(ComponentCreateView):
|
||||
class DeviceBayEditView(ObjectEditView):
|
||||
queryset = DeviceBay.objects.all()
|
||||
model_form = forms.DeviceBayForm
|
||||
template_name = 'dcim/device_component_edit.html'
|
||||
|
||||
|
||||
class DeviceBayDeleteView(ObjectDeleteView):
|
||||
|
@ -158,7 +158,7 @@ class CustomFieldModelSerializer(ValidatedModelSerializer):
|
||||
instance.custom_fields = {}
|
||||
for field in custom_fields:
|
||||
value = instance.cf.get(field.name)
|
||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT and value is not None:
|
||||
if field.type == CustomFieldTypeChoices.TYPE_SELECT and type(value) is CustomFieldChoice:
|
||||
instance.custom_fields[field.name] = CustomFieldChoiceSerializer(value).data
|
||||
else:
|
||||
instance.custom_fields[field.name] = value
|
||||
|
@ -101,24 +101,30 @@ class TaggedObjectSerializer(serializers.Serializer):
|
||||
tags = NestedTagSerializer(many=True, required=False)
|
||||
|
||||
def create(self, validated_data):
|
||||
tags = validated_data.pop('tags', [])
|
||||
tags = validated_data.pop('tags', None)
|
||||
instance = super().create(validated_data)
|
||||
|
||||
return self._save_tags(instance, tags)
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
tags = validated_data.pop('tags', [])
|
||||
tags = validated_data.pop('tags', None)
|
||||
|
||||
# Cache tags on instance for change logging
|
||||
instance._tags = tags
|
||||
instance._tags = tags or []
|
||||
|
||||
instance = super().update(instance, validated_data)
|
||||
|
||||
return self._save_tags(instance, tags)
|
||||
if tags is not None:
|
||||
return self._save_tags(instance, tags)
|
||||
return instance
|
||||
|
||||
def _save_tags(self, instance, tags):
|
||||
if tags:
|
||||
instance.tags.set(*[t.name for t in tags])
|
||||
else:
|
||||
instance.tags.clear()
|
||||
|
||||
return instance
|
||||
|
||||
|
@ -140,6 +140,7 @@ class ImageAttachmentViewSet(ModelViewSet):
|
||||
metadata_class = ContentTypeMetadata
|
||||
queryset = ImageAttachment.objects.all()
|
||||
serializer_class = serializers.ImageAttachmentSerializer
|
||||
filterset_class = filters.ImageAttachmentFilterSet
|
||||
|
||||
|
||||
#
|
||||
|
@ -1,4 +1,5 @@
|
||||
import django_filters
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db.models import Q
|
||||
|
||||
@ -7,7 +8,7 @@ from tenancy.models import Tenant, TenantGroup
|
||||
from utilities.filters import BaseFilterSet
|
||||
from virtualization.models import Cluster, ClusterGroup
|
||||
from .choices import *
|
||||
from .models import ConfigContext, CustomField, Graph, ExportTemplate, ObjectChange, JobResult, Tag
|
||||
from .models import ConfigContext, CustomField, ExportTemplate, Graph, ImageAttachment, JobResult, ObjectChange, Tag
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -17,6 +18,7 @@ __all__ = (
|
||||
'CustomFieldFilterSet',
|
||||
'ExportTemplateFilterSet',
|
||||
'GraphFilterSet',
|
||||
'ImageAttachmentFilterSet',
|
||||
'LocalConfigContextFilterSet',
|
||||
'ObjectChangeFilterSet',
|
||||
'TagFilterSet',
|
||||
@ -104,6 +106,13 @@ class ExportTemplateFilterSet(BaseFilterSet):
|
||||
fields = ['id', 'content_type', 'name', 'template_language']
|
||||
|
||||
|
||||
class ImageAttachmentFilterSet(BaseFilterSet):
|
||||
|
||||
class Meta:
|
||||
model = ImageAttachment
|
||||
fields = ['id', 'content_type', 'object_id', 'name']
|
||||
|
||||
|
||||
class TagFilterSet(BaseFilterSet):
|
||||
q = django_filters.CharFilter(
|
||||
method='search',
|
||||
@ -251,12 +260,21 @@ class ObjectChangeFilterSet(BaseFilterSet):
|
||||
label='Search',
|
||||
)
|
||||
time = django_filters.DateTimeFromToRangeFilter()
|
||||
user_id = django_filters.ModelMultipleChoiceFilter(
|
||||
queryset=User.objects.all(),
|
||||
label='User (ID)',
|
||||
)
|
||||
user = django_filters.ModelMultipleChoiceFilter(
|
||||
field_name='user__username',
|
||||
queryset=User.objects.all(),
|
||||
to_field_name='username',
|
||||
label='User name',
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = ObjectChange
|
||||
fields = [
|
||||
'id', 'user', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id',
|
||||
'object_repr',
|
||||
'id', 'user_name', 'request_id', 'action', 'changed_object_type', 'changed_object_id', 'object_repr',
|
||||
]
|
||||
|
||||
def search(self, queryset, name, value):
|
||||
|
@ -397,10 +397,11 @@ class ObjectChangeFilterForm(BootstrapMixin, forms.Form):
|
||||
required=False,
|
||||
widget=StaticSelect2()
|
||||
)
|
||||
user = DynamicModelMultipleChoiceField(
|
||||
user_id = DynamicModelMultipleChoiceField(
|
||||
queryset=User.objects.all(),
|
||||
required=False,
|
||||
display_field='username',
|
||||
label='User',
|
||||
widget=APISelectMultiple(
|
||||
api_url='/api/users/users/',
|
||||
)
|
||||
|
@ -1,7 +1,12 @@
|
||||
import time
|
||||
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.core.management.base import BaseCommand
|
||||
from django.utils import timezone
|
||||
|
||||
from extras.reports import get_reports
|
||||
from extras.choices import JobResultStatusChoices
|
||||
from extras.models import JobResult
|
||||
from extras.reports import get_reports, run_report
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
@ -20,15 +25,33 @@ class Command(BaseCommand):
|
||||
for report in report_list:
|
||||
if module_name in options['reports'] or report.full_name in options['reports']:
|
||||
|
||||
# Run the report and create a new ReportResult
|
||||
# Run the report and create a new JobResult
|
||||
self.stdout.write(
|
||||
"[{:%H:%M:%S}] Running {}...".format(timezone.now(), report.full_name)
|
||||
)
|
||||
report.run()
|
||||
|
||||
report_content_type = ContentType.objects.get(app_label='extras', model='report')
|
||||
job_result = JobResult.enqueue_job(
|
||||
run_report,
|
||||
report.full_name,
|
||||
report_content_type,
|
||||
None
|
||||
)
|
||||
|
||||
# Wait on the job to finish
|
||||
while job_result.status not in JobResultStatusChoices.TERMINAL_STATE_CHOICES:
|
||||
time.sleep(1)
|
||||
job_result = JobResult.objects.get(pk=job_result.pk)
|
||||
|
||||
# Report on success/failure
|
||||
status = self.style.ERROR('FAILED') if report.failed else self.style.SUCCESS('SUCCESS')
|
||||
for test_name, attrs in report.result.data.items():
|
||||
if job_result.status == JobResultStatusChoices.STATUS_FAILED:
|
||||
status = self.style.ERROR('FAILED')
|
||||
elif job_result == JobResultStatusChoices.STATUS_ERRORED:
|
||||
status = self.style.ERROR('ERRORED')
|
||||
else:
|
||||
status = self.style.SUCCESS('SUCCESS')
|
||||
|
||||
for test_name, attrs in job_result.data.items():
|
||||
self.stdout.write(
|
||||
"\t{}: {} success, {} info, {} warning, {} failure".format(
|
||||
test_name, attrs['success'], attrs['info'], attrs['warning'], attrs['failure']
|
||||
@ -37,6 +60,9 @@ class Command(BaseCommand):
|
||||
self.stdout.write(
|
||||
"[{:%H:%M:%S}] {}: {}".format(timezone.now(), report.full_name, status)
|
||||
)
|
||||
self.stdout.write(
|
||||
"[{:%H:%M:%S}] {}: Duration {}".format(timezone.now(), report.full_name, job_result.duration)
|
||||
)
|
||||
|
||||
# Wrap things up
|
||||
self.stdout.write(
|
||||
|
@ -1,11 +1,11 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import DeviceRole, Platform, Region, Site
|
||||
from dcim.models import DeviceRole, Platform, Rack, Region, Site
|
||||
from extras.choices import *
|
||||
from extras.filters import *
|
||||
from extras.utils import FeatureQuery
|
||||
from extras.models import ConfigContext, ExportTemplate, Graph, Tag
|
||||
from extras.models import ConfigContext, ExportTemplate, Graph, ImageAttachment, Tag
|
||||
from tenancy.models import Tenant, TenantGroup
|
||||
from virtualization.models import Cluster, ClusterGroup, ClusterType
|
||||
|
||||
@ -78,6 +78,84 @@ class ExportTemplateTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
|
||||
class ImageAttachmentTestCase(TestCase):
|
||||
queryset = ImageAttachment.objects.all()
|
||||
filterset = ImageAttachmentFilterSet
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
|
||||
site_ct = ContentType.objects.get(app_label='dcim', model='site')
|
||||
rack_ct = ContentType.objects.get(app_label='dcim', model='rack')
|
||||
|
||||
sites = (
|
||||
Site(name='Site 1', slug='site-1'),
|
||||
Site(name='Site 2', slug='site-2'),
|
||||
)
|
||||
Site.objects.bulk_create(sites)
|
||||
|
||||
racks = (
|
||||
Rack(name='Rack 1', site=sites[0]),
|
||||
Rack(name='Rack 2', site=sites[1]),
|
||||
)
|
||||
Rack.objects.bulk_create(racks)
|
||||
|
||||
image_attachments = (
|
||||
ImageAttachment(
|
||||
content_type=site_ct,
|
||||
object_id=sites[0].pk,
|
||||
name='Image Attachment 1',
|
||||
image='http://example.com/image1.png',
|
||||
image_height=100,
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=site_ct,
|
||||
object_id=sites[1].pk,
|
||||
name='Image Attachment 2',
|
||||
image='http://example.com/image2.png',
|
||||
image_height=100,
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=rack_ct,
|
||||
object_id=racks[0].pk,
|
||||
name='Image Attachment 3',
|
||||
image='http://example.com/image3.png',
|
||||
image_height=100,
|
||||
image_width=100
|
||||
),
|
||||
ImageAttachment(
|
||||
content_type=rack_ct,
|
||||
object_id=racks[1].pk,
|
||||
name='Image Attachment 4',
|
||||
image='http://example.com/image4.png',
|
||||
image_height=100,
|
||||
image_width=100
|
||||
)
|
||||
)
|
||||
ImageAttachment.objects.bulk_create(image_attachments)
|
||||
|
||||
def test_id(self):
|
||||
params = {'id': self.queryset.values_list('pk', flat=True)[:2]}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_name(self):
|
||||
params = {'name': ['Image Attachment 1', 'Image Attachment 2']}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_type(self):
|
||||
params = {'content_type': ContentType.objects.get(app_label='dcim', model='site').pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_content_type_and_object_id(self):
|
||||
params = {
|
||||
'content_type': ContentType.objects.get(app_label='dcim', model='site').pk,
|
||||
'object_id': [Site.objects.first().pk],
|
||||
}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
||||
class ConfigContextTestCase(TestCase):
|
||||
queryset = ConfigContext.objects.all()
|
||||
filterset = ConfigContextFilterSet
|
||||
|
@ -59,3 +59,21 @@ class TaggedItemTest(APITestCase):
|
||||
sorted([t.name for t in site.tags.all()]),
|
||||
sorted(["Foo", "Bar", "New Tag"])
|
||||
)
|
||||
|
||||
def test_clear_tagged_item(self):
|
||||
site = Site.objects.create(
|
||||
name='Test Site',
|
||||
slug='test-site'
|
||||
)
|
||||
site.tags.add("Foo", "Bar", "Baz")
|
||||
data = {
|
||||
'tags': []
|
||||
}
|
||||
self.add_permissions('dcim.change_site')
|
||||
url = reverse('dcim-api:site-detail', kwargs={'pk': site.pk})
|
||||
|
||||
response = self.client.patch(url, data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
self.assertEqual(len(response.data['tags']), 0)
|
||||
site = Site.objects.get(pk=response.data['id'])
|
||||
self.assertEqual(len(site.tags.all()), 0)
|
||||
|
@ -315,7 +315,7 @@ class ReportListView(ContentTypePermissionRequiredMixin, View):
|
||||
Retrieve all of the available reports from disk and the recorded JobResult (if any) for each.
|
||||
"""
|
||||
def get_required_permission(self):
|
||||
return 'extras.view_reportresult'
|
||||
return 'extras.view_report'
|
||||
|
||||
def get(self, request):
|
||||
|
||||
@ -347,7 +347,7 @@ class ReportView(ContentTypePermissionRequiredMixin, View):
|
||||
Display a single Report and its associated JobResult (if any).
|
||||
"""
|
||||
def get_required_permission(self):
|
||||
return 'extras.view_reportresult'
|
||||
return 'extras.view_report'
|
||||
|
||||
def get(self, request, module, name):
|
||||
|
||||
|
@ -247,7 +247,7 @@ class PrefixViewSet(CustomFieldModelViewSet):
|
||||
|
||||
class IPAddressViewSet(CustomFieldModelViewSet):
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags',
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'nat_outside', 'tags', 'assigned_object'
|
||||
)
|
||||
serializer_class = serializers.IPAddressSerializer
|
||||
filterset_class = filters.IPAddressFilterSet
|
||||
|
@ -641,11 +641,11 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
self.initial['primary_for_parent'] = True
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
# Cannot select both a device interface and a VM interface
|
||||
if self.cleaned_data.get('interface') and self.cleaned_data.get('vminterface'):
|
||||
raise forms.ValidationError("Cannot select both a device interface and a virtual machine interface")
|
||||
self.instance.assigned_object = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
|
||||
# Primary IP assignment is only available if an interface has been assigned.
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
@ -655,26 +655,21 @@ class IPAddressForm(BootstrapMixin, TenancyForm, ReturnURLForm, CustomFieldModel
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
||||
# Set assigned object
|
||||
interface = self.cleaned_data.get('interface') or self.cleaned_data.get('vminterface')
|
||||
if interface:
|
||||
self.instance.assigned_object = interface
|
||||
|
||||
ipaddress = super().save(*args, **kwargs)
|
||||
|
||||
# Assign/clear this IPAddress as the primary for the associated Device/VirtualMachine.
|
||||
interface = self.instance.assigned_object
|
||||
if interface and self.cleaned_data['primary_for_parent']:
|
||||
if ipaddress.address.version == 4:
|
||||
interface.parent.primary_ip4 = ipaddress
|
||||
else:
|
||||
interface.primary_ip6 = ipaddress
|
||||
interface.parent.primary_ip6 = ipaddress
|
||||
interface.parent.save()
|
||||
elif interface and ipaddress.address.version == 4 and interface.parent.primary_ip4 == ipaddress:
|
||||
interface.parent.primary_ip4 = None
|
||||
interface.parent.save()
|
||||
elif interface and ipaddress.address.version == 6 and interface.parent.primary_ip6 == ipaddress:
|
||||
interface.parent.primary_ip4 = None
|
||||
interface.parent.primary_ip6 = None
|
||||
interface.parent.save()
|
||||
|
||||
return ipaddress
|
||||
|
@ -726,30 +726,18 @@ class IPAddress(ChangeLoggedModel, CustomFieldModel):
|
||||
})
|
||||
|
||||
# Check for primary IP assignment that doesn't match the assigned device/VM
|
||||
if self.pk and type(self.assigned_object) is Interface:
|
||||
if self.pk:
|
||||
device = Device.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
if device:
|
||||
if self.assigned_object is None:
|
||||
if getattr(self.assigned_object, 'device', None) != device:
|
||||
raise ValidationError({
|
||||
'interface': f"IP address is primary for device {device} but not assigned to an interface"
|
||||
'interface': f"IP address is primary for device {device} but not assigned to it!"
|
||||
})
|
||||
elif self.assigned_object.device != device:
|
||||
raise ValidationError({
|
||||
'interface': f"IP address is primary for device {device} but assigned to "
|
||||
f"{self.assigned_object.device} ({self.assigned_object})"
|
||||
})
|
||||
elif self.pk and type(self.assigned_object) is VMInterface:
|
||||
vm = VirtualMachine.objects.filter(Q(primary_ip4=self) | Q(primary_ip6=self)).first()
|
||||
if vm:
|
||||
if self.assigned_object is None:
|
||||
if getattr(self.assigned_object, 'virtual_machine', None) != vm:
|
||||
raise ValidationError({
|
||||
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to an "
|
||||
f"interface"
|
||||
})
|
||||
elif self.assigned_object.virtual_machine != vm:
|
||||
raise ValidationError({
|
||||
'vminterface': f"IP address is primary for virtual machine {vm} but assigned to "
|
||||
f"{self.assigned_object.virtual_machine} ({self.assigned_object})"
|
||||
'vminterface': f"IP address is primary for virtual machine {vm} but not assigned to it!"
|
||||
})
|
||||
|
||||
# Validate IP status selection
|
||||
@ -997,13 +985,20 @@ class VLAN(ChangeLoggedModel, CustomFieldModel):
|
||||
def get_status_class(self):
|
||||
return self.STATUS_CLASS_MAP[self.status]
|
||||
|
||||
def get_members(self):
|
||||
# Return all interfaces assigned to this VLAN
|
||||
def get_interfaces(self):
|
||||
# Return all device interfaces assigned to this VLAN
|
||||
return Interface.objects.filter(
|
||||
Q(untagged_vlan_id=self.pk) |
|
||||
Q(tagged_vlans=self.pk)
|
||||
).distinct()
|
||||
|
||||
def get_vminterfaces(self):
|
||||
# Return all VM interfaces assigned to this VLAN
|
||||
return VMInterface.objects.filter(
|
||||
Q(untagged_vlan_id=self.pk) |
|
||||
Q(tagged_vlans=self.pk)
|
||||
).distinct()
|
||||
|
||||
|
||||
@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
|
||||
class Service(ChangeLoggedModel, CustomFieldModel):
|
||||
|
@ -4,6 +4,7 @@ from django_tables2.utils import Accessor
|
||||
from dcim.models import Interface
|
||||
from tenancy.tables import COL_TENANT
|
||||
from utilities.tables import BaseTable, BooleanColumn, ButtonsColumn, TagColumn, ToggleColumn
|
||||
from virtualization.models import VMInterface
|
||||
from .models import Aggregate, IPAddress, Prefix, RIR, Role, Service, VLAN, VLANGroup, VRF
|
||||
|
||||
RIR_UTILIZATION = """
|
||||
@ -67,11 +68,7 @@ IPADDRESS_LINK = """
|
||||
"""
|
||||
|
||||
IPADDRESS_ASSIGN_LINK = """
|
||||
{% if request.GET %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ request.GET.interface }}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
||||
{% else %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?interface={{ record.interface.pk }}&return_url={{ request.path }}">{{ record }}</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'ipam:ipaddress_edit' pk=record.pk %}?{% if request.GET.interface %}interface={{ request.GET.interface }}{% elif request.GET.vminterface %}vminterface={{ request.GET.vminterface }}{% endif %}&return_url={{ request.GET.return_url }}">{{ record }}</a>
|
||||
"""
|
||||
|
||||
VRF_LINK = """
|
||||
@ -103,7 +100,7 @@ VLAN_LINK = """
|
||||
"""
|
||||
|
||||
VLAN_PREFIXES = """
|
||||
{% for prefix in record.prefixes.unrestricted %}
|
||||
{% for prefix in record.prefixes.all %}
|
||||
<a href="{% url 'ipam:prefix' pk=prefix.pk %}">{{ prefix }}</a>{% if not forloop.last %}<br />{% endif %}
|
||||
{% empty %}
|
||||
—
|
||||
@ -128,9 +125,11 @@ VLANGROUP_ADD_VLAN = """
|
||||
{% endwith %}
|
||||
"""
|
||||
|
||||
VLAN_MEMBER_UNTAGGED = """
|
||||
VLAN_MEMBER_TAGGED = """
|
||||
{% if record.untagged_vlan_id == vlan.pk %}
|
||||
<i class="glyphicon glyphicon-ok">
|
||||
<span class="text-danger"><i class="fa fa-close"></i></span>
|
||||
{% else %}
|
||||
<span class="text-success"><i class="fa fa-check"></i></span>
|
||||
{% endif %}
|
||||
"""
|
||||
|
||||
@ -387,15 +386,23 @@ class IPAddressTable(BaseTable):
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=TENANT_LINK
|
||||
)
|
||||
assigned = tables.BooleanColumn(
|
||||
accessor='assigned_object_id',
|
||||
verbose_name='Assigned'
|
||||
assigned_object = tables.Column(
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name='Interface'
|
||||
)
|
||||
assigned_object_parent = tables.Column(
|
||||
accessor='assigned_object__parent',
|
||||
linkify=True,
|
||||
orderable=False,
|
||||
verbose_name='Interface Parent'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = IPAddress
|
||||
fields = (
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned', 'dns_name', 'description',
|
||||
'pk', 'address', 'vrf', 'status', 'role', 'tenant', 'assigned_object', 'assigned_object_parent', 'dns_name',
|
||||
'description',
|
||||
)
|
||||
row_attrs = {
|
||||
'class': lambda record: 'success' if not isinstance(record, IPAddress) else '',
|
||||
@ -411,6 +418,10 @@ class IPAddressDetailTable(IPAddressTable):
|
||||
tenant = tables.TemplateColumn(
|
||||
template_code=COL_TENANT
|
||||
)
|
||||
assigned = BooleanColumn(
|
||||
accessor='assigned_object_id',
|
||||
verbose_name='Assigned'
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='ipam:ipaddress_list'
|
||||
)
|
||||
@ -545,15 +556,15 @@ class VLANDetailTable(VLANTable):
|
||||
default_columns = ('pk', 'vid', 'site', 'group', 'name', 'prefixes', 'tenant', 'status', 'role', 'description')
|
||||
|
||||
|
||||
class VLANMemberTable(BaseTable):
|
||||
parent = tables.LinkColumn(
|
||||
order_by=['device', 'virtual_machine']
|
||||
)
|
||||
class VLANMembersTable(BaseTable):
|
||||
"""
|
||||
Base table for Interface and VMInterface assignments
|
||||
"""
|
||||
name = tables.LinkColumn(
|
||||
verbose_name='Interface'
|
||||
)
|
||||
untagged = tables.TemplateColumn(
|
||||
template_code=VLAN_MEMBER_UNTAGGED,
|
||||
tagged = tables.TemplateColumn(
|
||||
template_code=VLAN_MEMBER_TAGGED,
|
||||
orderable=False
|
||||
)
|
||||
actions = tables.TemplateColumn(
|
||||
@ -562,9 +573,21 @@ class VLANMemberTable(BaseTable):
|
||||
verbose_name=''
|
||||
)
|
||||
|
||||
|
||||
class VLANDevicesTable(VLANMembersTable):
|
||||
device = tables.LinkColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = Interface
|
||||
fields = ('parent', 'name', 'untagged', 'actions')
|
||||
fields = ('device', 'name', 'tagged', 'actions')
|
||||
|
||||
|
||||
class VLANVirtualMachinesTable(VLANMembersTable):
|
||||
virtual_machine = tables.LinkColumn()
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
fields = ('virtual_machine', 'name', 'tagged', 'actions')
|
||||
|
||||
|
||||
class InterfaceVLANTable(BaseTable):
|
||||
|
@ -90,7 +90,8 @@ urlpatterns = [
|
||||
path('vlans/edit/', views.VLANBulkEditView.as_view(), name='vlan_bulk_edit'),
|
||||
path('vlans/delete/', views.VLANBulkDeleteView.as_view(), name='vlan_bulk_delete'),
|
||||
path('vlans/<int:pk>/', views.VLANView.as_view(), name='vlan'),
|
||||
path('vlans/<int:pk>/members/', views.VLANMembersView.as_view(), name='vlan_members'),
|
||||
path('vlans/<int:pk>/interfaces/', views.VLANInterfacesView.as_view(), name='vlan_interfaces'),
|
||||
path('vlans/<int:pk>/vm-interfaces/', views.VLANVMInterfacesView.as_view(), name='vlan_vminterfaces'),
|
||||
path('vlans/<int:pk>/edit/', views.VLANEditView.as_view(), name='vlan_edit'),
|
||||
path('vlans/<int:pk>/delete/', views.VLANDeleteView.as_view(), name='vlan_delete'),
|
||||
path('vlans/<int:pk>/changelog/', ObjectChangeLogView.as_view(), name='vlan_changelog', kwargs={'model': VLAN}),
|
||||
|
@ -493,7 +493,7 @@ class PrefixBulkDeleteView(BulkDeleteView):
|
||||
|
||||
class IPAddressListView(ObjectListView):
|
||||
queryset = IPAddress.objects.prefetch_related(
|
||||
'vrf__tenant', 'tenant', 'nat_inside'
|
||||
'vrf__tenant', 'tenant', 'nat_inside', 'assigned_object'
|
||||
)
|
||||
filterset = filters.IPAddressFilterSet
|
||||
filterset_form = forms.IPAddressFilterForm
|
||||
@ -582,7 +582,7 @@ class IPAddressAssignView(ObjectView):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
|
||||
# Redirect user if an interface has not been provided
|
||||
if 'interface' not in request.GET:
|
||||
if 'interface' not in request.GET and 'vminterface' not in request.GET:
|
||||
return redirect('ipam:ipaddress_add')
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
@ -609,7 +609,7 @@ class IPAddressAssignView(ObjectView):
|
||||
return render(request, 'ipam/ipaddress_assign.html', {
|
||||
'form': form,
|
||||
'table': table,
|
||||
'return_url': request.GET.get('return_url', ''),
|
||||
'return_url': request.GET.get('return_url'),
|
||||
})
|
||||
|
||||
|
||||
@ -749,15 +749,13 @@ class VLANView(ObjectView):
|
||||
})
|
||||
|
||||
|
||||
class VLANMembersView(ObjectView):
|
||||
class VLANInterfacesView(ObjectView):
|
||||
queryset = VLAN.objects.all()
|
||||
|
||||
def get(self, request, pk):
|
||||
|
||||
vlan = get_object_or_404(self.queryset, pk=pk)
|
||||
members = vlan.get_members().restrict(request.user, 'view').prefetch_related('device', 'virtual_machine')
|
||||
|
||||
members_table = tables.VLANMemberTable(members)
|
||||
interfaces = vlan.get_interfaces().prefetch_related('device')
|
||||
members_table = tables.VLANDevicesTable(interfaces)
|
||||
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
@ -765,10 +763,31 @@ class VLANMembersView(ObjectView):
|
||||
}
|
||||
RequestConfig(request, paginate).configure(members_table)
|
||||
|
||||
return render(request, 'ipam/vlan_members.html', {
|
||||
return render(request, 'ipam/vlan_interfaces.html', {
|
||||
'vlan': vlan,
|
||||
'members_table': members_table,
|
||||
'active_tab': 'members',
|
||||
'active_tab': 'interfaces',
|
||||
})
|
||||
|
||||
|
||||
class VLANVMInterfacesView(ObjectView):
|
||||
queryset = VLAN.objects.all()
|
||||
|
||||
def get(self, request, pk):
|
||||
vlan = get_object_or_404(self.queryset, pk=pk)
|
||||
interfaces = vlan.get_vminterfaces().prefetch_related('virtual_machine')
|
||||
members_table = tables.VLANVirtualMachinesTable(interfaces)
|
||||
|
||||
paginate = {
|
||||
'paginator_class': EnhancedPaginator,
|
||||
'per_page': request.GET.get('per_page', settings.PAGINATE_COUNT)
|
||||
}
|
||||
RequestConfig(request, paginate).configure(members_table)
|
||||
|
||||
return render(request, 'ipam/vlan_vminterfaces.html', {
|
||||
'vlan': vlan,
|
||||
'members_table': members_table,
|
||||
'active_tab': 'vminterfaces',
|
||||
})
|
||||
|
||||
|
||||
|
@ -33,7 +33,6 @@ REDIS = {
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
@ -44,7 +43,6 @@ REDIS = {
|
||||
# 'SENTINEL_SERVICE': 'netbox',
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
@ -232,6 +230,9 @@ RELEASE_CHECK_URL = None
|
||||
# this setting is derived from the installed location.
|
||||
# REPORTS_ROOT = '/opt/netbox/netbox/reports'
|
||||
|
||||
# Maximum execution time for background tasks, in seconds.
|
||||
RQ_DEFAULT_TIMEOUT = 300
|
||||
|
||||
# The file path where custom scripts will be stored. A trailing slash is not needed. Note that the default value of
|
||||
# this setting is derived from the installed location.
|
||||
# SCRIPTS_ROOT = '/opt/netbox/netbox/scripts'
|
||||
|
@ -24,7 +24,6 @@ REDIS = {
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 0,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
},
|
||||
'caching': {
|
||||
@ -32,7 +31,6 @@ REDIS = {
|
||||
'PORT': 6379,
|
||||
'PASSWORD': '',
|
||||
'DATABASE': 1,
|
||||
'DEFAULT_TIMEOUT': 300,
|
||||
'SSL': False,
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ from django.core.validators import URLValidator
|
||||
# Environment setup
|
||||
#
|
||||
|
||||
VERSION = '2.9.2-dev'
|
||||
VERSION = '2.9.4'
|
||||
|
||||
# Hostname
|
||||
HOSTNAME = platform.node()
|
||||
@ -110,6 +110,7 @@ REMOTE_AUTH_HEADER = getattr(configuration, 'REMOTE_AUTH_HEADER', 'HTTP_REMOTE_U
|
||||
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
|
||||
RELEASE_CHECK_TIMEOUT = getattr(configuration, 'RELEASE_CHECK_TIMEOUT', 24 * 3600)
|
||||
REPORTS_ROOT = getattr(configuration, 'REPORTS_ROOT', os.path.join(BASE_DIR, 'reports')).rstrip('/')
|
||||
RQ_DEFAULT_TIMEOUT = getattr(configuration, 'RQ_DEFAULT_TIMEOUT', 300)
|
||||
SCRIPTS_ROOT = getattr(configuration, 'SCRIPTS_ROOT', os.path.join(BASE_DIR, 'scripts')).rstrip('/')
|
||||
SESSION_FILE_PATH = getattr(configuration, 'SESSION_FILE_PATH', None)
|
||||
SHORT_DATE_FORMAT = getattr(configuration, 'SHORT_DATE_FORMAT', 'Y-m-d')
|
||||
@ -220,10 +221,13 @@ TASKS_REDIS_USING_SENTINEL = all([
|
||||
len(TASKS_REDIS_SENTINELS) > 0
|
||||
])
|
||||
TASKS_REDIS_SENTINEL_SERVICE = TASKS_REDIS.get('SENTINEL_SERVICE', 'default')
|
||||
TASKS_REDIS_SENTINEL_TIMEOUT = TASKS_REDIS.get('SENTINEL_TIMEOUT', 10)
|
||||
TASKS_REDIS_PASSWORD = TASKS_REDIS.get('PASSWORD', '')
|
||||
TASKS_REDIS_DATABASE = TASKS_REDIS.get('DATABASE', 0)
|
||||
TASKS_REDIS_DEFAULT_TIMEOUT = TASKS_REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
TASKS_REDIS_SSL = TASKS_REDIS.get('SSL', False)
|
||||
# TODO: Remove in v2.10 (see #5171)
|
||||
if 'DEFAULT_TIMEOUT' in TASKS_REDIS:
|
||||
warnings.warn('DEFAULT_TIMEOUT is no longer supported under REDIS configuration. Set RQ_DEFAULT_TIMEOUT instead.')
|
||||
|
||||
# Caching
|
||||
if 'caching' not in REDIS:
|
||||
@ -241,7 +245,6 @@ CACHING_REDIS_USING_SENTINEL = all([
|
||||
CACHING_REDIS_SENTINEL_SERVICE = CACHING_REDIS.get('SENTINEL_SERVICE', 'default')
|
||||
CACHING_REDIS_PASSWORD = CACHING_REDIS.get('PASSWORD', '')
|
||||
CACHING_REDIS_DATABASE = CACHING_REDIS.get('DATABASE', 0)
|
||||
CACHING_REDIS_DEFAULT_TIMEOUT = CACHING_REDIS.get('DEFAULT_TIMEOUT', 300)
|
||||
CACHING_REDIS_SSL = CACHING_REDIS.get('SSL', False)
|
||||
|
||||
|
||||
@ -549,7 +552,7 @@ if TASKS_REDIS_USING_SENTINEL:
|
||||
'PASSWORD': TASKS_REDIS_PASSWORD,
|
||||
'SOCKET_TIMEOUT': None,
|
||||
'CONNECTION_KWARGS': {
|
||||
'socket_connect_timeout': TASKS_REDIS_DEFAULT_TIMEOUT
|
||||
'socket_connect_timeout': TASKS_REDIS_SENTINEL_TIMEOUT
|
||||
},
|
||||
}
|
||||
else:
|
||||
@ -558,8 +561,8 @@ else:
|
||||
'PORT': TASKS_REDIS_PORT,
|
||||
'DB': TASKS_REDIS_DATABASE,
|
||||
'PASSWORD': TASKS_REDIS_PASSWORD,
|
||||
'DEFAULT_TIMEOUT': TASKS_REDIS_DEFAULT_TIMEOUT,
|
||||
'SSL': TASKS_REDIS_SSL,
|
||||
'DEFAULT_TIMEOUT': RQ_DEFAULT_TIMEOUT,
|
||||
}
|
||||
|
||||
RQ_QUEUES = {
|
||||
|
@ -11,11 +11,8 @@
|
||||
<div class="row noprint">
|
||||
<div class="col-sm-8 col-md-9">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site' slug=device.site.slug %}">{{ device.site }}</a></li>
|
||||
{% if device.rack %}
|
||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ device.site.slug }}">Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a></li>
|
||||
{% endif %}
|
||||
<li><a href="{% url 'dcim:device_list' %}">Devices</a></li>
|
||||
<li><a href="{% url 'dcim:device_list' %}?site={{ device.site.slug }}">{{ device.site }}</a></li>
|
||||
{% if device.parent_bay %}
|
||||
<li><a href="{% url 'dcim:device' pk=device.parent_bay.device.pk %}">{{ device.parent_bay.device }}</a></li>
|
||||
<li>{{ device.parent_bay }}</li>
|
||||
@ -101,7 +98,7 @@
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'inventory' %} class="active"{% endif %}>
|
||||
<a href="{% url 'dcim:device_inventory' pk=device.pk %}">
|
||||
Inventory <span class="badge">{{ device.inventoryitems.unrestricted.count }}</span>
|
||||
Inventory <span class="badge">{{ device.inventoryitems.count }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% if perms.dcim.napalm_read_device %}
|
||||
@ -151,8 +148,10 @@
|
||||
<td>
|
||||
{% if device.rack %}
|
||||
{% if device.rack.group %}
|
||||
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% for group in device.rack.group.get_ancestors %}
|
||||
<a href="{{ group.get_absolute_url }}">{{ group }}</a> <i class="fa fa-caret-right"></i>
|
||||
{% endfor %}
|
||||
<a href="{{ device.rack.group.get_absolute_url }}">{{ device.rack.group }}</a> <i class="fa fa-caret-right"></i>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:rack' pk=device.rack.pk %}">{{ device.rack }}</a>
|
||||
{% else %}
|
||||
|
16
netbox/templates/dcim/device_component_edit.html
Normal file
16
netbox/templates/dcim/device_component_edit.html
Normal file
@ -0,0 +1,16 @@
|
||||
{% extends 'utilities/obj_edit.html' %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block form_fields %}
|
||||
{% if form.instance.device %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required" for="id_device">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{{ form.instance.device.get_absolute_url }}">{{ form.instance.device }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% render_form form %}
|
||||
{% endblock %}
|
@ -23,6 +23,7 @@
|
||||
<div class="panel-body">
|
||||
{% render_field form.region %}
|
||||
{% render_field form.site %}
|
||||
{% render_field form.rack_group %}
|
||||
{% render_field form.rack %}
|
||||
{% if obj.device_type.is_child_device and obj.parent_bay %}
|
||||
<div class="form-group">
|
||||
|
@ -66,7 +66,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_consoleport %}
|
||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<a href="{% url 'dcim:consoleport_edit' pk=cp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -68,7 +68,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_consoleserverport %}
|
||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<a href="{% url 'dcim:consoleserverport_edit' pk=csp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -52,7 +52,7 @@
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true" title="Install device"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}" class="btn btn-info btn-xs">
|
||||
<a href="{% url 'dcim:devicebay_edit' pk=devicebay.pk %}?return_url={{ device.get_absolute_url }}" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true" title="Edit device bay"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -81,7 +81,7 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_poweroutlet %}
|
||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}" title="Edit outlet" class="btn btn-info btn-xs">
|
||||
<a href="{% url 'dcim:poweroutlet_edit' pk=po.pk %}?return_url={{ device.get_absolute_url }}" title="Edit outlet" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -78,7 +78,7 @@
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if perms.dcim.change_powerport %}
|
||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<a href="{% url 'dcim:powerport_edit' pk=pp.pk %}?return_url={{ device.get_absolute_url }}" title="Edit port" class="btn btn-info btn-xs">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
|
@ -5,6 +5,16 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Interface</strong></div>
|
||||
<div class="panel-body">
|
||||
{% if form.instance.device %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required" for="id_device">Device</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{{ form.instance.device.get_absolute_url }}">{{ form.instance.device }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.label %}
|
||||
{% render_field form.type %}
|
||||
@ -14,6 +24,11 @@
|
||||
{% render_field form.mtu %}
|
||||
{% render_field form.mgmt_only %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.mode %}
|
||||
{% render_field form.untagged_vlan %}
|
||||
{% render_field form.tagged_vlans %}
|
||||
|
@ -11,6 +11,12 @@
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:rack_list' %}">Racks</a></li>
|
||||
<li><a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}">{{ rack.site }}</a></li>
|
||||
{% if rack.group %}
|
||||
{% for group in rack.group.get_ancestors %}
|
||||
<li><a href="{{ group.get_absolute_url }}">{{ group }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="{{ rack.group.get_absolute_url }}">{{ rack.group }}</a></li>
|
||||
{% endif %}
|
||||
<li>{{ rack }}</li>
|
||||
</ol>
|
||||
</div>
|
||||
@ -87,7 +93,10 @@
|
||||
<td>Group</td>
|
||||
<td>
|
||||
{% if rack.group %}
|
||||
<a href="{% url 'dcim:rack_list' %}?site={{ rack.site.slug }}&group={{ rack.group.slug }}">{{ rack.group }}</a>
|
||||
{% for group in rack.group.get_ancestors %}
|
||||
<a href="{{ group.get_absolute_url }}">{{ group }}</a> <i class="fa fa-caret-right"></i>
|
||||
{% endfor %}
|
||||
<a href="{{ rack.group.get_absolute_url }}">{{ rack.group }}</a>
|
||||
{% else %}
|
||||
<span class="text-muted">None</span>
|
||||
{% endif %}
|
||||
|
@ -3,12 +3,18 @@
|
||||
{% load static %}
|
||||
|
||||
{% block content %}
|
||||
<div class="btn-group pull-right noprint" role="group">
|
||||
<div class="btn-toolbar pull-right noprint" role="toolbar">
|
||||
<button class="btn btn-default toggle-images" selected="selected">
|
||||
<span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show Images
|
||||
</button>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-default{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-default{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
|
||||
</div>
|
||||
<div class="btn-group" role="group">
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request %}" class="btn btn-default{% if not reverse %} active{% endif %}">Normal</a>
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}{% querystring request reverse='true' %}" class="btn btn-default{% if reverse %} active{% endif %}">Reversed</a>
|
||||
</div>
|
||||
</div>
|
||||
<h1>{% block title %}Rack Elevations{% endblock %}</h1>
|
||||
<div class="row">
|
||||
|
@ -12,7 +12,7 @@
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{% url 'dcim:site_list' %}">Sites</a></li>
|
||||
{% if site.region %}
|
||||
{% for region in site.region.get_ancestors.unrestricted %}
|
||||
{% for region in site.region.get_ancestors %}
|
||||
<li><a href="{{ region.get_absolute_url }}">{{ region }}</a></li>
|
||||
{% endfor %}
|
||||
<li><a href="{{ site.region.get_absolute_url }}">{{ site.region }}</a></li>
|
||||
@ -86,7 +86,7 @@
|
||||
<td>Region</td>
|
||||
<td>
|
||||
{% if site.region %}
|
||||
{% for region in site.region.get_ancestors.unrestricted %}
|
||||
{% for region in site.region.get_ancestors %}
|
||||
<a href="{{ region.get_absolute_url }}">{{ region }}</a>
|
||||
<i class="fa fa-angle-right"></i>
|
||||
{% endfor %}
|
||||
@ -255,7 +255,7 @@
|
||||
<table class="table table-hover panel-body">
|
||||
{% for rg in rack_groups %}
|
||||
<tr>
|
||||
<td><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
|
||||
<td style="padding-left: {{ rg.level }}8px"><i class="fa fa-fw fa-folder-o"></i> <a href="{{ rg.get_absolute_url }}">{{ rg }}</a></td>
|
||||
<td>{{ rg.rack_count }}</td>
|
||||
<td class="text-right noprint">
|
||||
<a href="{% url 'dcim:rack_elevation_list' %}?group_id={{ rg.pk }}" class="btn btn-xs btn-primary" title="View elevations">
|
||||
|
@ -5,14 +5,15 @@
|
||||
A module import error occurred during this request. Common causes include the following:
|
||||
</p>
|
||||
<p>
|
||||
<i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be missing one or more required
|
||||
Python packages. These packages are listed in <code>requirements.txt</code> and are normally installed as part
|
||||
of the installation or upgrade process. To verify installed packages, run <code>pip freeze</code> from the
|
||||
console and compare the output to the list of required packages.
|
||||
<i class="fa fa-warning"></i> <strong>Missing required packages</strong> - This installation of NetBox might be
|
||||
missing one or more required Python packages. These packages are listed in <code>requirements.txt</code> and
|
||||
<code>local_requirements.txt</code>, and are normally installed as part of the installation or upgrade process.
|
||||
To verify installed packages, run <code>pip freeze</code> from the console and compare the output to the list of
|
||||
required packages.
|
||||
</p>
|
||||
<p>
|
||||
<i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation has recently been upgraded,
|
||||
check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This ensures that the new code is
|
||||
running.
|
||||
<i class="fa fa-warning"></i> <strong>WSGI service not restarted after upgrade</strong> - If this installation
|
||||
has recently been upgraded, check that the WSGI service (e.g. gunicorn or uWSGI) has been restarted. This
|
||||
ensures that the new code is running.
|
||||
</p>
|
||||
{% endblock %}
|
||||
|
@ -276,7 +276,7 @@
|
||||
<div class="panel-heading">
|
||||
<strong>Reports</strong>
|
||||
</div>
|
||||
{% if report_results and perms.extras.view_reportresult %}
|
||||
{% if report_results and perms.extras.view_report %}
|
||||
<table class="table table-hover panel-body">
|
||||
{% for result in report_results %}
|
||||
<tr>
|
||||
@ -285,7 +285,7 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
{% elif perms.extras.view_reportresult %}
|
||||
{% elif perms.extras.view_report %}
|
||||
<div class="panel-body text-muted">
|
||||
None found
|
||||
</div>
|
||||
|
@ -518,7 +518,7 @@
|
||||
<li{% if not perms.extras.view_script %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:script_list' %}">Scripts</a>
|
||||
</li>
|
||||
<li{% if not perms.extras.view_reportresult %} class="disabled"{% endif %}>
|
||||
<li{% if not perms.extras.view_report %} class="disabled"{% endif %}>
|
||||
<a href="{% url 'extras:report_list' %}">Reports</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -5,18 +5,22 @@
|
||||
{% for section_name, menu_items in registry.plugin_menu_items.items %}
|
||||
<li class="dropdown-header">{{ section_name }}</li>
|
||||
{% for menu_item in menu_items %}
|
||||
<li{% if menu_item.permissions and not request.user|has_perms:menu_item.permissions %} class="disabled"{% endif %}>
|
||||
{% if menu_item.buttons %}
|
||||
<div class="buttons pull-right">
|
||||
{% for button in menu_item.buttons %}
|
||||
{% if not button.permissions or request.user|has_perms:button.permissions %}
|
||||
<a href="{% url button.link %}" class="btn btn-xs btn-{{ button.color }}" title="{{ button.title }}"><i class="{{ button.icon_class }}"></i></a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
|
||||
</li>
|
||||
{% if not menu_item.permissions or request.user|has_perms:menu_item.permissions %}
|
||||
<li>
|
||||
{% if menu_item.buttons %}
|
||||
<div class="buttons pull-right">
|
||||
{% for button in menu_item.buttons %}
|
||||
{% if not button.permissions or request.user|has_perms:button.permissions %}
|
||||
<a href="{% url button.link %}" class="btn btn-xs btn-{{ button.color }}" title="{{ button.title }}"><i class="{{ button.icon_class }}"></i></a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url menu_item.link %}">{{ menu_item.link_text }}</a>
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="disabled"><a href="#">{{ menu_item.link_text }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% if not forloop.last %}
|
||||
<li class="divider"></li>
|
||||
|
@ -52,8 +52,11 @@
|
||||
<li role="presentation"{% if not active_tab %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vlan' pk=vlan.pk %}">VLAN</a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'members' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vlan_members' pk=vlan.pk %}">Members <span class="badge">{{ vlan.get_members.count }}</span></a>
|
||||
<li role="presentation"{% if active_tab == 'interfaces' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vlan_interfaces' pk=vlan.pk %}">Device Interfaces <span class="badge">{{ vlan.get_interfaces.count }}</span></a>
|
||||
</li>
|
||||
<li role="presentation"{% if active_tab == 'vminterfaces' %} class="active"{% endif %}>
|
||||
<a href="{% url 'ipam:vlan_vminterfaces' pk=vlan.pk %}">VM Interfaces <span class="badge">{{ vlan.get_vminterfaces.count }}</span></a>
|
||||
</li>
|
||||
{% if perms.extras.view_objectchange %}
|
||||
<li role="presentation"{% if active_tab == 'changelog' %} class="active"{% endif %}>
|
||||
|
@ -1,11 +1,9 @@
|
||||
{% extends 'ipam/vlan.html' %}
|
||||
|
||||
{% block title %}{{ block.super }} - Members{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='VLAN Members' parent=vlan %}
|
||||
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='Device Interfaces' parent=vlan %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
9
netbox/templates/ipam/vlan_vminterfaces.html
Normal file
9
netbox/templates/ipam/vlan_vminterfaces.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% extends 'ipam/vlan.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
{% include 'utilities/obj_table.html' with table=members_table table_template='panel_table.html' heading='Virtual Machine Interfaces' parent=vlan %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
@ -31,7 +31,9 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>{{ obj_type|capfirst }}</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_form form %}
|
||||
{% block form_fields %}
|
||||
{% render_form form %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -2,7 +2,7 @@
|
||||
<tr class="interface{% if not iface.enabled %} danger{% endif %}" id="interface_{{ iface.name }}">
|
||||
|
||||
{# Checkbox #}
|
||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
||||
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||
<td class="pk">
|
||||
<input name="pk" type="checkbox" value="{{ iface.pk }}" />
|
||||
</td>
|
||||
@ -48,12 +48,12 @@
|
||||
<i class="glyphicon glyphicon-plus" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.change_interface %}
|
||||
{% if perms.virtualization.change_vminterface %}
|
||||
<a href="{% url 'virtualization:vminterface_edit' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-info btn-xs" title="Edit interface">
|
||||
<i class="glyphicon glyphicon-pencil" aria-hidden="true"></i>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.delete_interface %}
|
||||
{% if perms.virtualization.delete_vminterface %}
|
||||
<a href="{% url 'virtualization:vminterface_delete' pk=iface.pk %}?return_url={{ virtualmachine.get_absolute_url }}" class="btn btn-danger btn-xs" title="Delete interface">
|
||||
<i class="glyphicon glyphicon-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
@ -65,7 +65,7 @@
|
||||
{% if ipaddresses %}
|
||||
<tr class="ipaddresses">
|
||||
{# Placeholder #}
|
||||
{% if perms.virtualization.change_interface or perms.virtualization.delete_interface %}
|
||||
{% if perms.virtualization.change_vminterface or perms.virtualization.delete_vminterface %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
{% load helpers %}
|
||||
{% load form_helpers %}
|
||||
|
||||
{% block title %}Create {{ component_type }} ({{ parent }}){% endblock %}
|
||||
{% block title %}Create {{ component_type }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<form action="" method="post" class="form form-horizontal">
|
||||
|
@ -5,14 +5,34 @@
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Interface</strong></div>
|
||||
<div class="panel-body">
|
||||
{% if form.instance.virtual_machine %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label required" for="id_device">Virtual Machine</label>
|
||||
<div class="col-md-9">
|
||||
<p class="form-control-static">
|
||||
<a href="{{ form.instance.virtual_machine.get_absolute_url }}">{{ form.instance.virtual_machine }}</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% render_field form.name %}
|
||||
{% render_field form.enabled %}
|
||||
{% render_field form.mac_address %}
|
||||
{% render_field form.mtu %}
|
||||
{% render_field form.description %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>802.1Q Switching</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.mode %}
|
||||
{% render_field form.untagged_vlan %}
|
||||
{% render_field form.tagged_vlans %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading"><strong>Tags</strong></div>
|
||||
<div class="panel-body">
|
||||
{% render_field form.tags %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -38,6 +38,10 @@ class LoginView(View):
|
||||
def get(self, request):
|
||||
form = LoginForm(request)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
logger = logging.getLogger('netbox.auth.login')
|
||||
return self.redirect_to_next(request, logger)
|
||||
|
||||
return render(request, self.template_name, {
|
||||
'form': form,
|
||||
})
|
||||
@ -49,12 +53,6 @@ class LoginView(View):
|
||||
if form.is_valid():
|
||||
logger.debug("Login form validation was successful")
|
||||
|
||||
# Determine where to direct user after successful login
|
||||
redirect_to = request.POST.get('next', reverse('home'))
|
||||
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
||||
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
|
||||
redirect_to = reverse('home')
|
||||
|
||||
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
|
||||
# last_login time upon authentication.
|
||||
if settings.MAINTENANCE_MODE:
|
||||
@ -66,8 +64,7 @@ class LoginView(View):
|
||||
logger.info(f"User {request.user} successfully authenticated")
|
||||
messages.info(request, "Logged in as {}.".format(request.user))
|
||||
|
||||
logger.debug(f"Redirecting user to {redirect_to}")
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
return self.redirect_to_next(request, logger)
|
||||
|
||||
else:
|
||||
logger.debug("Login form validation failed")
|
||||
@ -76,6 +73,19 @@ class LoginView(View):
|
||||
'form': form,
|
||||
})
|
||||
|
||||
def redirect_to_next(self, request, logger):
|
||||
if request.method == "POST":
|
||||
redirect_to = request.POST.get('next', reverse('home'))
|
||||
else:
|
||||
redirect_to = request.GET.get('next', reverse('home'))
|
||||
|
||||
if redirect_to and not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()):
|
||||
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_to}")
|
||||
redirect_to = reverse('home')
|
||||
|
||||
logger.debug(f"Redirecting user to {redirect_to}")
|
||||
return HttpResponseRedirect(redirect_to)
|
||||
|
||||
|
||||
class LogoutView(View):
|
||||
"""
|
||||
|
@ -141,7 +141,7 @@ class APISelect(SelectWithDisabled):
|
||||
key = f'data-query-param-{name}'
|
||||
|
||||
values = json.loads(self.attrs.get(key, '[]'))
|
||||
if type(value) is list:
|
||||
if type(value) in (list, tuple):
|
||||
values.extend([str(v) for v in value])
|
||||
else:
|
||||
values.append(str(value))
|
||||
|
@ -44,7 +44,7 @@ class BaseTable(tables.Table):
|
||||
self.columns.show(name)
|
||||
else:
|
||||
self.columns.hide(name)
|
||||
self.sequence = columns
|
||||
self.sequence = [c for c in columns if c in self.base_columns]
|
||||
|
||||
# Always include PK and actions column, if defined on the table
|
||||
if pk:
|
||||
@ -114,12 +114,12 @@ class BooleanColumn(tables.Column):
|
||||
character.
|
||||
"""
|
||||
def render(self, value):
|
||||
if value is True:
|
||||
if value:
|
||||
rendered = '<span class="text-success"><i class="fa fa-check"></i></span>'
|
||||
elif value is False:
|
||||
rendered = '<span class="text-danger"><i class="fa fa-close"></i></span>'
|
||||
else:
|
||||
elif value is None:
|
||||
rendered = '<span class="text-muted">—</span>'
|
||||
else:
|
||||
rendered = '<span class="text-danger"><i class="fa fa-close"></i></span>'
|
||||
return mark_safe(rendered)
|
||||
|
||||
|
||||
|
@ -266,7 +266,7 @@ class APIViewTestCases:
|
||||
response = self.client.patch(url, update_data, format='json', **self.header)
|
||||
self.assertHttpStatus(response, status.HTTP_200_OK)
|
||||
instance.refresh_from_db()
|
||||
self.assertInstanceEqual(instance, self.update_data, api=True)
|
||||
self.assertInstanceEqual(instance, update_data, api=True)
|
||||
|
||||
class DeleteObjectViewTestCase(APITestCase):
|
||||
|
||||
|
@ -945,7 +945,7 @@ class BulkEditView(GetReturnURLMixin, ObjectPermissionRequiredMixin, View):
|
||||
|
||||
# ManyToManyFields
|
||||
elif isinstance(model_field, ManyToManyField):
|
||||
if form.cleaned_data[name].count() > 0:
|
||||
if form.cleaned_data[name]:
|
||||
getattr(obj, name).set(form.cleaned_data[name])
|
||||
# Normal fields
|
||||
elif form.cleaned_data[name] not in (None, ''):
|
||||
|
@ -684,7 +684,7 @@ class VMInterfaceCSVForm(CSVModelForm):
|
||||
return self.cleaned_data['enabled']
|
||||
|
||||
|
||||
class VMInterfaceBulkEditForm(BootstrapMixin, BulkEditForm):
|
||||
class VMInterfaceBulkEditForm(BootstrapMixin, AddRemoveTagsForm, BulkEditForm):
|
||||
pk = forms.ModelMultipleChoiceField(
|
||||
queryset=VMInterface.objects.all(),
|
||||
widget=forms.MultipleHiddenInput()
|
||||
|
@ -83,6 +83,7 @@ def replicate_interfaces(apps, schema_editor):
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('dcim', '0082_3569_interface_fields'),
|
||||
('ipam', '0037_ipaddress_assignment'),
|
||||
('virtualization', '0015_vminterface'),
|
||||
]
|
||||
|
@ -154,11 +154,14 @@ class VMInterfaceTable(BaseInterfaceTable):
|
||||
name = tables.Column(
|
||||
linkify=True
|
||||
)
|
||||
tags = TagColumn(
|
||||
url_name='virtualization:vminterface_list'
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
model = VMInterface
|
||||
fields = (
|
||||
'pk', 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'ip_addresses',
|
||||
'pk', 'virtual_machine', 'name', 'enabled', 'mac_address', 'mtu', 'description', 'tags', 'ip_addresses',
|
||||
'untagged_vlan', 'tagged_vlans',
|
||||
)
|
||||
default_columns = ('pk', 'virtual_machine', 'name', 'enabled', 'description')
|
||||
|
Loading…
Reference in New Issue
Block a user