Merge branch 'feature' into feat/12882-contact-assignments-tags

# Conflicts:
#	requirements.txt
This commit is contained in:
Abhimanyu Saharan 2023-08-01 09:15:41 +05:30
commit 7308e39d3e
511 changed files with 132070 additions and 4754 deletions

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.5.5
placeholder: v3.5.7
validations:
required: true
- type: dropdown

View File

@ -14,7 +14,7 @@ body:
attributes:
label: NetBox version
description: What version of NetBox are you currently running?
placeholder: v3.5.5
placeholder: v3.5.7
validations:
required: true
- type: dropdown

View File

@ -68,8 +68,13 @@ When defining a permission constraint, administrators may use the special token
The `$user` token can be used only as a constraint value, or as an item within a list of values. It cannot be modified or extended to reference specific user attributes.
### Default Permissions
#### Example Constraint Definitions
!!! info "This feature was introduced in NetBox v3.6."
While permissions are typically assigned to specific groups and/or users, it is also possible to define a set of default permissions that are applied to _all_ authenticated users. This is done using the [`DEFAULT_PERMISSIONS`](../configuration/security.md#default_permissions) configuration parameter. Note that statically configuring permissions for specific users or groups is **not** supported.
### Example Constraint Definitions
| Constraints | Description |
| ----------- | ----------- |

View File

@ -90,6 +90,38 @@ CSRF_TRUSTED_ORIGINS = (
---
## DEFAULT_PERMISSIONS
!!! info "This parameter was introduced in NetBox v3.6."
Default:
```python
{
'users.view_token': ({'user': '$user'},),
'users.add_token': ({'user': '$user'},),
'users.change_token': ({'user': '$user'},),
'users.delete_token': ({'user': '$user'},),
}
```
This parameter defines object permissions that are applied automatically to _any_ authenticated user, regardless of what permissions have been defined in the database. By default, this parameter is defined to allow all users to manage their own API tokens, however it can be overriden for any purpose.
For example, to allow all users to create a device role beginning with the word "temp," you could configure the following:
```python
DEFAULT_PERMISSIONS = {
'dcim.add_devicerole': (
{'name__startswith': 'temp'},
)
}
```
!!! warning
Setting a custom value for this parameter will overwrite the default permission mapping shown above. If you want to retain the default mapping, be sure to reproduce it in your custom configuration.
---
## EXEMPT_VIEW_PERMISSIONS
Default: Empty list

View File

@ -60,7 +60,7 @@ NetBox supports limited custom validation for custom field values. Following are
### Custom Selection Fields
Each custom selection field must have at least two choices. These are specified as a comma-separated list. Choices appear in forms in the order they are listed. Note that choice values are saved exactly as they appear, so it's best to avoid superfluous punctuation or symbols where possible.
Each custom selection field must designate a [choice set](../models/extras/customfieldchoiceset.md) containing at least two choices. These are specified as a comma-separated list.
If a default value is specified for a selection field, it must exactly match one of the provided choices. The value of a multiple selection field will always return a list, even if only one value is selected.

View File

@ -8,6 +8,10 @@ The registry can be inspected by importing `registry` from `extras.registry`.
## Stores
### `counter_fields`
A dictionary mapping of models to foreign keys with which cached counter fields are associated.
### `data_backends`
A dictionary mapping data backend types to their respective classes. These are used to interact with [remote data sources](../models/core/datasource.md).

View File

@ -38,7 +38,7 @@ An example hierarchy might look like this:
* 100.64.16.1/24 (address)
* 100.64.16.2/24 (address)
* 100.64.16.3/24 (address)
* 100.64.16.9/24 (prefix)
* 100.64.19.0/24 (prefix)
* 100.64.32.0/20 (prefix)
* 100.64.32.1/24 (address)
* 100.64.32.10-99/24 (range)

View File

@ -55,6 +55,9 @@ Within the shell, enter the following commands to create the database and user (
CREATE DATABASE netbox;
CREATE USER netbox WITH PASSWORD 'J5brHrAXFLQSif0K';
ALTER DATABASE netbox OWNER TO netbox;
-- the next two commands are needed on PostgreSQL 15 and later
\connect netbox;
GRANT CREATE ON SCHEMA public TO netbox;
```
!!! danger "Use a strong password"

View File

@ -48,36 +48,40 @@ Download the [latest stable release](https://github.com/netbox-community/netbox/
Download and extract the latest version:
```no-highlight
wget https://github.com/netbox-community/netbox/archive/vX.Y.Z.tar.gz
sudo tar -xzf vX.Y.Z.tar.gz -C /opt
sudo ln -sfn /opt/netbox-X.Y.Z/ /opt/netbox
# Set $NEWVER to the NetBox version being installed
NEWVER=3.5.0
wget https://github.com/netbox-community/netbox/archive/v$NEWVER.tar.gz
sudo tar -xzf v$NEWVER.tar.gz -C /opt
sudo ln -sfn /opt/netbox-$NEWVER/ /opt/netbox
```
Copy `local_requirements.txt`, `configuration.py`, and `ldap_config.py` (if present) from the current installation to the new version:
```no-highlight
sudo cp /opt/netbox-X.Y.Z/local_requirements.txt /opt/netbox/
sudo cp /opt/netbox-X.Y.Z/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
sudo cp /opt/netbox-X.Y.Z/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
# Set $OLDVER to the NetBox version currently installed
NEWVER=3.4.9
sudo cp /opt/netbox-$OLDVER/local_requirements.txt /opt/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/configuration.py /opt/netbox/netbox/netbox/
sudo cp /opt/netbox-$OLDVER/netbox/netbox/ldap_config.py /opt/netbox/netbox/netbox/
```
Be sure to replicate your uploaded media as well. (The exact action necessary will depend on where you choose to store your media, but in general moving or copying the media directory will suffice.)
```no-highlight
sudo cp -pr /opt/netbox-X.Y.Z/netbox/media/ /opt/netbox/netbox/
sudo cp -pr /opt/netbox-$OLDVER/netbox/media/ /opt/netbox/netbox/
```
Also make sure to copy or link any custom scripts and reports that you've made. Note that if these are stored outside the project root, you will not need to copy them. (Check the `SCRIPTS_ROOT` and `REPORTS_ROOT` parameters in the configuration file above if you're unsure.)
```no-highlight
sudo cp -r /opt/netbox-X.Y.Z/netbox/scripts /opt/netbox/netbox/
sudo cp -r /opt/netbox-X.Y.Z/netbox/reports /opt/netbox/netbox/
sudo cp -r /opt/netbox-$OLDVER/netbox/scripts /opt/netbox/netbox/
sudo cp -r /opt/netbox-$OLDVER/netbox/reports /opt/netbox/netbox/
```
If you followed the original installation guide to set up gunicorn, be sure to copy its configuration as well:
```no-highlight
sudo cp /opt/netbox-X.Y.Z/gunicorn.py /opt/netbox/
sudo cp /opt/netbox-$OLDVER/gunicorn.py /opt/netbox/
```
### Option B: Clone the Git Repository

View File

@ -87,6 +87,10 @@ Each device may designate one primary IPv4 address and/or one primary IPv6 addre
!!! tip
NetBox will prefer IPv6 addresses over IPv4 addresses by default. This can be changed by setting the `PREFER_IPV4` configuration parameter.
### Out-of-band (OOB) IP Address
Each device may designate its out-of-band IP address. Out-of-band IPs are typically used to access network infrastructure via a physically separate management network.
### Cluster
If this device will serve as a host for a virtualization [cluster](../virtualization/cluster.md), it can be assigned here. (Host devices can also be assigned by editing the cluster.)

View File

@ -79,9 +79,9 @@ Controls how and whether the custom field is displayed within the NetBox user in
The default value to populate for the custom field when creating new objects (optional). This value must be expressed as JSON. If this is a choice or multi-choice field, this must be one of the available choices.
### Choices
### Choice Set
For choice and multi-choice custom fields only. A comma-delimited list of the available choices.
For selection and multi-select custom fields only, this is the [set of choices](./customfieldchoiceset.md) which are valid for the field.
### Cloneable

View File

@ -0,0 +1,27 @@
# Custom Field Choice Sets
Single- and multi-selection [custom fields](../../customization/custom-fields.md) must define a set of valid choices from which the user may choose when defining the field value. These choices are defined as sets that may be reused among multiple custom fields.
A choice set must define a base choice set and/or a set of arbitrary extra choices.
## Fields
### Name
The human-friendly name of the choice set.
### Base Choices
The set of pre-defined choices to include. Available sets are listed below. This is an optional setting.
* IATA airport codes
* ISO 3166 - Two-letter country codes
* UN/LOCODE - Five-character location identifiers
### Extra Choices
A set of custom choices that will be appended to the base choice set (if any).
### Order Alphabetically
If enabled, the choices list will be automatically ordered alphabetically. If disabled, choices will appear in the order in which they were defined.

View File

@ -1,6 +1,37 @@
# NetBox v3.5
## v3.5.6 (FUTURE)
## v3.5.8 (FUTURE)
---
## v3.5.7 (2023-07-28)
### Enhancements
* [#11803](https://github.com/netbox-community/netbox/issues/11803) - Move non-rack devices list to a separate tab under the rack view
* [#12625](https://github.com/netbox-community/netbox/issues/12625) - Mask sensitive parameters when viewing a configured data source
* [#13009](https://github.com/netbox-community/netbox/issues/13009) - Add IEC 10609-1 and NBR 14136 power port & outlet types
* [#13097](https://github.com/netbox-community/netbox/issues/13097) - Implement a faster initial poll for report & script results
* [#13234](https://github.com/netbox-community/netbox/issues/13234) - Add 100GBASE-X-DSFP and 100GBASE-X-SFPDD interface types
### Bug Fixes
* [#13051](https://github.com/netbox-community/netbox/issues/13051) - Fix Markdown support for table cell alignment
* [#13167](https://github.com/netbox-community/netbox/issues/13167) - Fix missing script results when fetched via REST API
* [#13233](https://github.com/netbox-community/netbox/issues/13233) - Remove extraneous VLAN group field from bulk edit form for interfaces
* [#13237](https://github.com/netbox-community/netbox/issues/13237) - Permit unauthenticated access to content types REST API endpoint when `LOGIN_REQUIRED` is false
* [#13285](https://github.com/netbox-community/netbox/issues/13285) - Fix exception when importing device type missing rack unit height value
---
## v3.5.6 (2023-07-10)
### Bug Fixes
* [#13061](https://github.com/netbox-community/netbox/issues/13061) - Fix display of last result for scripts & reports with a custom name defined
* [#13096](https://github.com/netbox-community/netbox/issues/13096) - Hide scheduling fields for all scripts with scheduling disabled
* [#13105](https://github.com/netbox-community/netbox/issues/13105) - Fix exception when attempting to allocate next available IP address from prefix marked as utilized
* [#13116](https://github.com/netbox-community/netbox/issues/13116) - Catch ProgrammingError exception when starting NetBox without pre-populated content types
---

View File

@ -7,16 +7,64 @@
* PostgreSQL 11 is no longer supported (due to adopting Django 4.2). NetBox v3.6 requires PostgreSQL 12 or later.
* The `napalm_driver` and `napalm_args` fields (which were deprecated in v3.5) have been removed from the platform model.
### New Features
#### Relocated Admin Views ([#12589](https://github.com/netbox-community/netbox/issues/12589), [#12590](https://github.com/netbox-community/netbox/issues/12590), [#12591](https://github.com/netbox-community/netbox/issues/12591), [#13044](https://github.com/netbox-community/netbox/issues/13044))
Management views for the following object types, previously available only under the backend admin interface, have been relocated to the primary user interface:
* Users
* Groups
* Object permissions
* API tokens
* Configuration revisions
The admin UI is scheduled for removal in NetBox v4.0.
#### Configurable Default Permissions ([#13038](https://github.com/netbox-community/netbox/issues/13038))
Administrators now have the option of configuring a set of default permissions to be applied to _all_ users, regardless of explicit permission or group assignment. This is accomplished by defining the `DEFAULT_PERMISSIONS` configuration parameter.
#### User Bookmarks ([#8248](https://github.com/netbox-community/netbox/issues/8248))
Users can now bookmark their most commonly-visited objects in NetBox. Bookmarks will display both on the dashboard (if configured) and on a user-specific bookmarks view.
#### Custom Field Choice Sets ([#12988](https://github.com/netbox-community/netbox/issues/12988))
Select and multi-select custom fields now employ discrete, reusable choice sets containing the valid options for each field. A choice set may be shared by multiple custom fields. Additionally, each choice within a set can now specify both a value and a human-friendly label (see [#13241](https://github.com/netbox-community/netbox/issues/13241)).
#### Pre-Defined Location Choices for Custom Fields ([#12194](https://github.com/netbox-community/netbox/issues/12194))
Users now have the option to employ one of several pre-defined choice sets when creating a custom field. These include:
* IATA airport codes
* ISO 3166 country codes
* UN/LOCODE identifiers
Extra choices may also be appended to the pre-defined sets as needed.
#### Restrict Tag Usage by Object Type ([#11541](https://github.com/netbox-community/netbox/issues/11541))
Tags may now be restricted to use with designated object types. Tags that have no specific object types assigned may be used with any object that supports tag assignment.
### Enhancements
* [#6347](https://github.com/netbox-community/netbox/issues/6347) - Cache the number of assigned components for devices and virtual machines
* [#8137](https://github.com/netbox-community/netbox/issues/8137) - Add a field for designating the out-of-band (OOB) IP address for devices
* [#10197](https://github.com/netbox-community/netbox/issues/10197) - Cache the number of member devices on each virtual chassis
* [#11305](https://github.com/netbox-community/netbox/issues/11305) - Add GPS coordinate fields to the device model
* [#12175](https://github.com/netbox-community/netbox/issues/12175) - Permit racks to start numbering at values greater than one
* [#12210](https://github.com/netbox-community/netbox/issues/12210) - Add tenancy assignment for power feeds
* [#13170](https://github.com/netbox-community/netbox/issues/13170) - Add `rf_role` to InterfaceTemplate
* [#13269](https://github.com/netbox-community/netbox/issues/13269) - Cache the number of assigned component templates for device types
### Other Changes
* Work has begun on introducing translation and localization support in NetBox. This work is being performed in preparation for release 4.0.
* [#9077](https://github.com/netbox-community/netbox/issues/9077) - Prevent the errant execution of dangerous instance methods in Django templates
* [#11766](https://github.com/netbox-community/netbox/issues/11766) - Remove obsolete custom `ChoiceField` and `MultipleChoiceField` classes
* [#12180](https://github.com/netbox-community/netbox/issues/12180) - All API endpoints for available objects (e.g. IP addresses) now inherit from a common parent view
* [#12237](https://github.com/netbox-community/netbox/issues/12237) - Upgrade Django to v4.2
* [#12794](https://github.com/netbox-community/netbox/issues/12794) - Avoid direct imports of Django's stock user model
* [#12320](https://github.com/netbox-community/netbox/issues/12320) - Remove obsolete fields `napalm_driver` and `napalm_args` from Platform
* [#12964](https://github.com/netbox-community/netbox/issues/12964) - Drop support for PostgreSQL
* [#12964](https://github.com/netbox-community/netbox/issues/12964) - Drop support for PostgreSQL 11

View File

View File

@ -0,0 +1,27 @@
# Generated by Django 4.1.10 on 2023-07-30 17:49
from django.db import migrations
class Migration(migrations.Migration):
initial = True
dependencies = [
('users', '0004_netboxgroup_netboxuser'),
]
operations = [
migrations.CreateModel(
name='UserToken',
fields=[
],
options={
'verbose_name': 'token',
'proxy': True,
'indexes': [],
'constraints': [],
},
bases=('users.token',),
),
]

View File

15
netbox/account/models.py Normal file
View File

@ -0,0 +1,15 @@
from django.urls import reverse
from users.models import Token
class UserToken(Token):
"""
Proxy model for users to manage their own API tokens.
"""
class Meta:
proxy = True
verbose_name = 'token'
def get_absolute_url(self):
return reverse('account:usertoken', args=[self.pk])

55
netbox/account/tables.py Normal file
View File

@ -0,0 +1,55 @@
from django.utils.translation import gettext as _
from account.models import UserToken
from netbox.tables import NetBoxTable, columns
__all__ = (
'UserTokenTable',
)
TOKEN = """<samp><span id="token_{{ record.pk }}">{{ record }}</span></samp>"""
ALLOWED_IPS = """{{ value|join:", " }}"""
COPY_BUTTON = """
{% if settings.ALLOW_TOKEN_RETRIEVAL %}
{% copy_content record.pk prefix="token_" color="success" %}
{% endif %}
"""
class UserTokenTable(NetBoxTable):
"""
Table for users to manager their own API tokens under account views.
"""
key = columns.TemplateColumn(
verbose_name=_('Key'),
template_code=TOKEN,
)
write_enabled = columns.BooleanColumn(
verbose_name=_('Write Enabled')
)
created = columns.DateColumn(
verbose_name=_('Created'),
)
expires = columns.DateColumn(
verbose_name=_('Expires'),
)
last_used = columns.DateTimeColumn(
verbose_name=_('Last Used'),
)
allowed_ips = columns.TemplateColumn(
verbose_name=_('Allowed IPs'),
template_code=ALLOWED_IPS
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=COPY_BUTTON
)
class Meta(NetBoxTable.Meta):
model = UserToken
fields = (
'pk', 'id', 'key', 'description', 'write_enabled', 'created', 'expires', 'last_used', 'allowed_ips',
)

18
netbox/account/urls.py Normal file
View File

@ -0,0 +1,18 @@
from django.urls import include, path
from utilities.urls import get_model_urls
from . import views
app_name = 'account'
urlpatterns = [
# Account views
path('profile/', views.ProfileView.as_view(), name='profile'),
path('bookmarks/', views.BookmarkListView.as_view(), name='bookmarks'),
path('preferences/', views.UserConfigView.as_view(), name='preferences'),
path('password/', views.ChangePasswordView.as_view(), name='change_password'),
path('api-tokens/', views.UserTokenListView.as_view(), name='usertoken_list'),
path('api-tokens/add/', views.UserTokenEditView.as_view(), name='usertoken_add'),
path('api-tokens/<int:pk>/', include(get_model_urls('account', 'usertoken'))),
]

298
netbox/account/views.py Normal file
View File

@ -0,0 +1,298 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, redirect
from django.shortcuts import render, resolve_url
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.http import url_has_allowed_host_and_scheme, urlencode
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic import View
from social_core.backends.utils import load_backends
from account.models import UserToken
from extras.models import Bookmark, ObjectChange
from extras.tables import BookmarkTable, ObjectChangeTable
from netbox.authentication import get_auth_backend_display, get_saml_idps
from netbox.config import get_config
from netbox.views import generic
from users import forms, tables
from users.models import UserConfig
from utilities.views import register_model_view
#
# Login/logout
#
class LoginView(View):
"""
Perform user authentication via the web UI.
"""
template_name = 'login.html'
@method_decorator(sensitive_post_parameters('password'))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def gen_auth_data(self, name, url, params):
display_name, icon_name = get_auth_backend_display(name)
return {
'display_name': display_name,
'icon_name': icon_name,
'url': f'{url}?{urlencode(params)}',
}
def get_auth_backends(self, request):
auth_backends = []
saml_idps = get_saml_idps()
for name in load_backends(settings.AUTHENTICATION_BACKENDS).keys():
url = reverse('social:begin', args=[name])
params = {}
if next := request.GET.get('next'):
params['next'] = next
if name.lower() == 'saml' and saml_idps:
for idp in saml_idps:
params['idp'] = idp
data = self.gen_auth_data(name, url, params)
data['display_name'] = f'{data["display_name"]} ({idp})'
auth_backends.append(data)
else:
auth_backends.append(self.gen_auth_data(name, url, params))
return auth_backends
def get(self, request):
form = forms.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,
'auth_backends': self.get_auth_backends(request),
})
def post(self, request):
logger = logging.getLogger('netbox.auth.login')
form = forms.LoginForm(request, data=request.POST)
if form.is_valid():
logger.debug("Login form validation was successful")
# If maintenance mode is enabled, assume the database is read-only, and disable updating the user's
# last_login time upon authentication.
if get_config().MAINTENANCE_MODE:
logger.warning("Maintenance mode enabled: disabling update of most recent login time")
user_logged_in.disconnect(update_last_login, dispatch_uid='update_last_login')
# Authenticate user
auth_login(request, form.get_user())
logger.info(f"User {request.user} successfully authenticated")
messages.success(request, f"Logged in as {request.user}.")
# Ensure the user has a UserConfig defined. (This should normally be handled by
# create_userconfig() on user creation.)
if not hasattr(request.user, 'config'):
config = get_config()
UserConfig(user=request.user, data=config.DEFAULT_USER_PREFERENCES).save()
return self.redirect_to_next(request, logger)
else:
logger.debug(f"Login form validation failed for username: {form['username'].value()}")
return render(request, self.template_name, {
'form': form,
'auth_backends': self.get_auth_backends(request),
})
def redirect_to_next(self, request, logger):
data = request.POST if request.method == "POST" else request.GET
redirect_url = data.get('next', settings.LOGIN_REDIRECT_URL)
if redirect_url and url_has_allowed_host_and_scheme(redirect_url, allowed_hosts=None):
logger.debug(f"Redirecting user to {redirect_url}")
else:
if redirect_url:
logger.warning(f"Ignoring unsafe 'next' URL passed to login form: {redirect_url}")
redirect_url = reverse('home')
return HttpResponseRedirect(redirect_url)
class LogoutView(View):
"""
Deauthenticate a web user.
"""
def get(self, request):
logger = logging.getLogger('netbox.auth.logout')
# Log out the user
username = request.user
auth_logout(request)
logger.info(f"User {username} has logged out")
messages.info(request, "You have logged out.")
# Delete session key cookie (if set) upon logout
response = HttpResponseRedirect(resolve_url(settings.LOGOUT_REDIRECT_URL))
response.delete_cookie('session_key')
return response
#
# User profiles
#
class ProfileView(LoginRequiredMixin, View):
template_name = 'account/profile.html'
def get(self, request):
# Compile changelog table
changelog = ObjectChange.objects.valid_models().restrict(request.user, 'view').filter(
user=request.user
).prefetch_related(
'changed_object_type'
)[:20]
changelog_table = ObjectChangeTable(changelog)
return render(request, self.template_name, {
'changelog_table': changelog_table,
'active_tab': 'profile',
})
class UserConfigView(LoginRequiredMixin, View):
template_name = 'account/preferences.html'
def get(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(instance=userconfig)
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
def post(self, request):
userconfig = request.user.config
form = forms.UserConfigForm(request.POST, instance=userconfig)
if form.is_valid():
form.save()
messages.success(request, "Your preferences have been updated.")
return redirect('account:preferences')
return render(request, self.template_name, {
'form': form,
'active_tab': 'preferences',
})
class ChangePasswordView(LoginRequiredMixin, View):
template_name = 'account/password.html'
def get(self, request):
# LDAP users cannot change their password here
if getattr(request.user, 'ldap_username', None):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('account:profile')
form = forms.PasswordChangeForm(user=request.user)
return render(request, self.template_name, {
'form': form,
'active_tab': 'password',
})
def post(self, request):
form = forms.PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
messages.success(request, "Your password has been changed successfully.")
return redirect('account:profile')
return render(request, self.template_name, {
'form': form,
'active_tab': 'change_password',
})
#
# Bookmarks
#
class BookmarkListView(LoginRequiredMixin, generic.ObjectListView):
table = BookmarkTable
template_name = 'account/bookmarks.html'
def get_queryset(self, request):
return Bookmark.objects.filter(user=request.user)
def get_extra_context(self, request):
return {
'active_tab': 'bookmarks',
}
#
# User views for token management
#
class UserTokenListView(LoginRequiredMixin, View):
def get(self, request):
tokens = UserToken.objects.filter(user=request.user)
table = tables.UserTokenTable(tokens)
table.configure(request)
return render(request, 'account/token_list.html', {
'tokens': tokens,
'active_tab': 'api-tokens',
'table': table,
})
@register_model_view(UserToken)
class UserTokenView(LoginRequiredMixin, View):
def get(self, request, pk):
token = get_object_or_404(UserToken.objects.filter(user=request.user), pk=pk)
key = token.key if settings.ALLOW_TOKEN_RETRIEVAL else None
return render(request, 'account/token.html', {
'object': token,
'key': key,
})
@register_model_view(UserToken, 'edit')
class UserTokenEditView(generic.ObjectEditView):
queryset = UserToken.objects.all()
form = forms.UserTokenForm
default_return_url = 'account:usertoken_list'
def alter_object(self, obj, request, url_args, url_kwargs):
if not obj.pk:
obj.user = request.user
return obj
@register_model_view(UserToken, 'delete')
class UserTokenDeleteView(generic.ObjectDeleteView):
queryset = UserToken.objects.all()
default_return_url = 'account:usertoken_list'

View File

@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
@ -16,12 +18,12 @@ class CircuitStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONED = 'decommissioned'
CHOICES = [
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_PROVISIONING, 'Provisioning', 'blue'),
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_OFFLINE, 'Offline', 'red'),
(STATUS_DEPROVISIONING, 'Deprovisioning', 'yellow'),
(STATUS_DECOMMISSIONED, 'Decommissioned', 'gray'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_PROVISIONING, _('Provisioning'), 'blue'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_OFFLINE, _('Offline'), 'red'),
(STATUS_DEPROVISIONING, _('Deprovisioning'), 'yellow'),
(STATUS_DECOMMISSIONED, _('Decommissioned'), 'gray'),
]

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitStatusChoices
from circuits.models import *
@ -26,12 +26,11 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField(
label=_('Comments')
)
comments = CommentField()
model = Provider
fieldsets = (
@ -44,16 +43,16 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm):
class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField(
label=_('Comments')
)
comments = CommentField()
model = ProviderAccount
fieldsets = (
@ -66,6 +65,7 @@ class ProviderAccountBulkEditForm(NetBoxModelBulkEditForm):
class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
required=False
)
@ -75,12 +75,11 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
label=_('Service ID')
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField(
label=_('Comments')
)
comments = CommentField()
model = ProviderNetwork
fieldsets = (
@ -93,6 +92,7 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm):
class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
@ -106,14 +106,17 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm):
class CircuitBulkEditForm(NetBoxModelBulkEditForm):
type = DynamicModelChoiceField(
label=_('Type'),
queryset=CircuitType.objects.all(),
required=False
)
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
required=False
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False,
query_params={
@ -121,19 +124,23 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
}
)
status = forms.ChoiceField(
label=_('Status'),
choices=add_blank_choice(CircuitStatusChoices),
required=False,
initial=''
)
tenant = DynamicModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False
)
install_date = forms.DateField(
label=_('Install date'),
required=False,
widget=DatePicker()
)
termination_date = forms.DateField(
label=_('Termination date'),
required=False,
widget=DatePicker()
)
@ -145,18 +152,17 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm):
)
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
comments = CommentField(
label=_('Comments')
)
comments = CommentField()
model = Circuit
fieldsets = (
('Circuit', ('provider', 'type', 'status', 'description')),
('Service Parameters', ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
('Tenancy', ('tenant',)),
(_('Circuit'), ('provider', 'type', 'status', 'description')),
(_('Service Parameters'), ('provider_account', 'install_date', 'termination_date', 'commit_rate')),
(_('Tenancy'), ('tenant',)),
)
nullable_fields = (
'tenant', 'commit_rate', 'description', 'comments',

View File

@ -3,7 +3,7 @@ from django import forms
from circuits.choices import CircuitStatusChoices
from circuits.models import *
from dcim.models import Site
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
from utilities.forms import BootstrapMixin
@ -31,6 +31,7 @@ class ProviderImportForm(NetBoxModelImportForm):
class ProviderAccountImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
to_field_name='name',
help_text=_('Assigned provider')
@ -45,6 +46,7 @@ class ProviderAccountImportForm(NetBoxModelImportForm):
class ProviderNetworkImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
to_field_name='name',
help_text=_('Assigned provider')
@ -67,26 +69,31 @@ class CircuitTypeImportForm(NetBoxModelImportForm):
class CircuitImportForm(NetBoxModelImportForm):
provider = CSVModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
to_field_name='name',
help_text=_('Assigned provider')
)
provider_account = CSVModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
to_field_name='name',
help_text=_('Assigned provider account'),
required=False
)
type = CSVModelChoiceField(
label=_('Type'),
queryset=CircuitType.objects.all(),
to_field_name='name',
help_text=_('Type of circuit')
)
status = CSVChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
help_text=_('Operational status')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
@ -103,11 +110,13 @@ class CircuitImportForm(NetBoxModelImportForm):
class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
required=False
)
provider_network = CSVModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
to_field_name='name',
required=False

View File

@ -23,9 +23,9 @@ class ProviderFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Provider
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('ASN', ('asn',)),
('Contacts', ('contact', 'contact_role', 'contact_group')),
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
(_('ASN'), ('asn',)),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -62,7 +62,7 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
model = ProviderAccount
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('provider_id', 'account')),
(_('Attributes'), ('provider_id', 'account')),
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
@ -70,6 +70,7 @@ class ProviderAccountFilterForm(NetBoxModelFilterSetForm):
label=_('Provider')
)
account = forms.CharField(
label=_('Account'),
required=False
)
tag = TagFilterField(model)
@ -79,7 +80,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
model = ProviderNetwork
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('provider_id', 'service_id')),
(_('Attributes'), ('provider_id', 'service_id')),
)
provider_id = DynamicModelMultipleChoiceField(
queryset=Provider.objects.all(),
@ -87,6 +88,7 @@ class ProviderNetworkFilterForm(NetBoxModelFilterSetForm):
label=_('Provider')
)
service_id = forms.CharField(
label=_('Service id'),
max_length=100,
required=False
)
@ -102,11 +104,11 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
model = Circuit
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Provider', ('provider_id', 'provider_account_id', 'provider_network_id')),
('Attributes', ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
(_('Provider'), ('provider_id', 'provider_account_id', 'provider_network_id')),
(_('Attributes'), ('type_id', 'status', 'install_date', 'termination_date', 'commit_rate')),
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
type_id = DynamicModelMultipleChoiceField(
queryset=CircuitType.objects.all(),
@ -135,6 +137,7 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
label=_('Provider network')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=CircuitStatusChoices,
required=False
)
@ -158,10 +161,12 @@ class CircuitFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFi
label=_('Site')
)
install_date = forms.DateField(
label=_('Install date'),
required=False,
widget=DatePicker
)
termination_date = forms.DateField(
label=_('Termination date'),
required=False,
widget=DatePicker
)

View File

@ -1,4 +1,4 @@
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from circuits.choices import CircuitCommitRateChoices, CircuitTerminationPortSpeedChoices
from circuits.models import *
@ -29,7 +29,7 @@ class ProviderForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
('Provider', ('name', 'slug', 'asns', 'description', 'tags')),
(_('Provider'), ('name', 'slug', 'asns', 'description', 'tags')),
)
class Meta:
@ -41,6 +41,7 @@ class ProviderForm(NetBoxModelForm):
class ProviderAccountForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all()
)
comments = CommentField()
@ -54,12 +55,13 @@ class ProviderAccountForm(NetBoxModelForm):
class ProviderNetworkForm(NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all()
)
comments = CommentField()
fieldsets = (
('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')),
(_('Provider Network'), ('provider', 'name', 'service_id', 'description', 'tags')),
)
class Meta:
@ -73,7 +75,7 @@ class CircuitTypeForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Circuit Type', (
(_('Circuit Type'), (
'name', 'slug', 'description', 'tags',
)),
)
@ -87,10 +89,12 @@ class CircuitTypeForm(NetBoxModelForm):
class CircuitForm(TenancyForm, NetBoxModelForm):
provider = DynamicModelChoiceField(
label=_('Provider'),
queryset=Provider.objects.all(),
selector=True
)
provider_account = DynamicModelChoiceField(
label=_('Provider account'),
queryset=ProviderAccount.objects.all(),
required=False,
query_params={
@ -103,9 +107,9 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
('Circuit', ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
('Service Parameters', ('install_date', 'termination_date', 'commit_rate')),
('Tenancy', ('tenant_group', 'tenant')),
(_('Circuit'), ('provider', 'provider_account', 'cid', 'type', 'status', 'description', 'tags')),
(_('Service Parameters'), ('install_date', 'termination_date', 'commit_rate')),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
class Meta:
@ -125,15 +129,18 @@ class CircuitForm(TenancyForm, NetBoxModelForm):
class CircuitTerminationForm(NetBoxModelForm):
circuit = DynamicModelChoiceField(
label=_('Circuit'),
queryset=Circuit.objects.all(),
selector=True
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False,
selector=True
)
provider_network = DynamicModelChoiceField(
label=_('Provider network'),
queryset=ProviderNetwork.objects.all(),
required=False,
selector=True

View File

@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from circuits.choices import *
from dcim.models import CabledObjectModel
@ -34,8 +34,8 @@ class Circuit(PrimaryModel):
"""
cid = models.CharField(
max_length=100,
verbose_name='Circuit ID',
help_text=_("Unique circuit ID")
verbose_name=_('circuit ID'),
help_text=_('Unique circuit ID')
)
provider = models.ForeignKey(
to='circuits.Provider',
@ -55,6 +55,7 @@ class Circuit(PrimaryModel):
related_name='circuits'
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=CircuitStatusChoices,
default=CircuitStatusChoices.STATUS_ACTIVE
@ -69,17 +70,17 @@ class Circuit(PrimaryModel):
install_date = models.DateField(
blank=True,
null=True,
verbose_name='Installed'
verbose_name=_('installed')
)
termination_date = models.DateField(
blank=True,
null=True,
verbose_name='Terminates'
verbose_name=_('terminates')
)
commit_rate = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Commit rate (Kbps)',
verbose_name=_('commit rate (Kbps)'),
help_text=_("Committed rate")
)
@ -162,7 +163,7 @@ class CircuitTermination(
term_side = models.CharField(
max_length=1,
choices=CircuitTerminationSideChoices,
verbose_name='Termination'
verbose_name=_('termination')
)
site = models.ForeignKey(
to='dcim.Site',
@ -179,30 +180,31 @@ class CircuitTermination(
null=True
)
port_speed = models.PositiveIntegerField(
verbose_name='Port speed (Kbps)',
verbose_name=_('port speed (Kbps)'),
blank=True,
null=True,
help_text=_("Physical circuit speed")
help_text=_('Physical circuit speed')
)
upstream_speed = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Upstream speed (Kbps)',
verbose_name=_('upstream speed (Kbps)'),
help_text=_('Upstream speed, if different from port speed')
)
xconnect_id = models.CharField(
max_length=50,
blank=True,
verbose_name='Cross-connect ID',
help_text=_("ID of the local cross-connect")
verbose_name=_('cross-connect ID'),
help_text=_('ID of the local cross-connect')
)
pp_info = models.CharField(
max_length=100,
blank=True,
verbose_name='Patch panel/port(s)',
help_text=_("Patch panel ID and port number(s)")
verbose_name=_('patch panel/port(s)'),
help_text=_('Patch panel ID and port number(s)')
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)

View File

@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.models import PrimaryModel
@ -19,11 +19,13 @@ class Provider(PrimaryModel):
stores information pertinent to the user's relationship with the Provider.
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True,
help_text=_("Full name of the provider")
help_text=_('Full name of the provider')
)
slug = models.SlugField(
verbose_name=_('slug'),
max_length=100,
unique=True
)
@ -61,9 +63,10 @@ class ProviderAccount(PrimaryModel):
)
account = models.CharField(
max_length=100,
verbose_name='Account ID'
verbose_name=_('account ID')
)
name = models.CharField(
verbose_name=_('name'),
max_length=100,
blank=True
)
@ -104,6 +107,7 @@ class ProviderNetwork(PrimaryModel):
unimportant to the user.
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
)
provider = models.ForeignKey(
@ -114,7 +118,7 @@ class ProviderNetwork(PrimaryModel):
service_id = models.CharField(
max_length=100,
blank=True,
verbose_name='Service ID'
verbose_name=_('service ID')
)
class Meta:

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from circuits.models import *
@ -24,7 +25,8 @@ CIRCUITTERMINATION_LINK = """
class CircuitTypeTable(NetBoxTable):
name = tables.Column(
linkify=True
linkify=True,
verbose_name=_('Name'),
)
tags = columns.TagColumn(
url_name='circuits:circuittype_list'
@ -32,7 +34,7 @@ class CircuitTypeTable(NetBoxTable):
circuit_count = columns.LinkedCountColumn(
viewname='circuits:circuit_list',
url_params={'type_id': 'pk'},
verbose_name='Circuits'
verbose_name=_('Circuits')
)
class Meta(NetBoxTable.Meta):
@ -46,28 +48,31 @@ class CircuitTypeTable(NetBoxTable):
class CircuitTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
cid = tables.Column(
linkify=True,
verbose_name='Circuit ID'
verbose_name=_('Circuit ID')
)
provider = tables.Column(
verbose_name=_('Provider'),
linkify=True
)
provider_account = tables.Column(
linkify=True,
verbose_name='Account'
verbose_name=_('Account')
)
status = columns.ChoiceFieldColumn()
termination_a = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side A'
verbose_name=_('Side A')
)
termination_z = tables.TemplateColumn(
template_code=CIRCUITTERMINATION_LINK,
verbose_name='Side Z'
verbose_name=_('Side Z')
)
commit_rate = CommitRateColumn(
verbose_name='Commit Rate'
verbose_name=_('Commit Rate')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='circuits:circuit_list'
)

View File

@ -1,4 +1,5 @@
import django_tables2 as tables
from django.utils.translation import gettext_lazy as _
from circuits.models import *
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin
@ -14,35 +15,38 @@ __all__ = (
class ProviderTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
accounts = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='Accounts'
verbose_name=_('Accounts')
)
account_count = columns.LinkedCountColumn(
accessor=tables.A('accounts__count'),
viewname='circuits:provideraccount_list',
url_params={'account_id': 'pk'},
verbose_name='Account Count'
verbose_name=_('Account Count')
)
asns = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs'
verbose_name=_('ASNs')
)
asn_count = columns.LinkedCountColumn(
accessor=tables.A('asns__count'),
viewname='ipam:asn_list',
url_params={'provider_id': 'pk'},
verbose_name='ASN Count'
verbose_name=_('ASN Count')
)
circuit_count = columns.LinkedCountColumn(
accessor=Accessor('count_circuits'),
viewname='circuits:circuit_list',
url_params={'provider_id': 'pk'},
verbose_name='Circuits'
verbose_name=_('Circuits')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='circuits:provider_list'
)
@ -58,19 +62,25 @@ class ProviderTable(ContactsColumnMixin, NetBoxTable):
class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
account = tables.Column(
linkify=True
linkify=True,
verbose_name=_('Account'),
)
name = tables.Column(
verbose_name=_('Name'),
)
name = tables.Column()
provider = tables.Column(
verbose_name=_('Provider'),
linkify=True
)
circuit_count = columns.LinkedCountColumn(
accessor=Accessor('count_circuits'),
viewname='circuits:circuit_list',
url_params={'provider_account_id': 'pk'},
verbose_name='Circuits'
verbose_name=_('Circuits')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='circuits:provideraccount_list'
)
@ -86,12 +96,16 @@ class ProviderAccountTable(ContactsColumnMixin, NetBoxTable):
class ProviderNetworkTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
provider = tables.Column(
verbose_name=_('Provider'),
linkify=True
)
comments = columns.MarkdownColumn()
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='circuits:providernetwork_list'
)

View File

@ -1,4 +1,4 @@
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
@ -63,12 +63,12 @@ class JobStatusChoices(ChoiceSet):
STATUS_FAILED = 'failed'
CHOICES = (
(STATUS_PENDING, 'Pending', 'cyan'),
(STATUS_SCHEDULED, 'Scheduled', 'gray'),
(STATUS_RUNNING, 'Running', 'blue'),
(STATUS_COMPLETED, 'Completed', 'green'),
(STATUS_ERRORED, 'Errored', 'red'),
(STATUS_FAILED, 'Failed', 'red'),
(STATUS_PENDING, _('Pending'), 'cyan'),
(STATUS_SCHEDULED, _('Scheduled'), 'gray'),
(STATUS_RUNNING, _('Running'), 'blue'),
(STATUS_COMPLETED, _('Completed'), 'green'),
(STATUS_ERRORED, _('Errored'), 'red'),
(STATUS_FAILED, _('Failed'), 'red'),
)
TERMINAL_STATE_CHOICES = (

View File

@ -41,6 +41,7 @@ def register_backend(name):
class DataBackend:
parameters = {}
sensitive_parameters = []
def __init__(self, url, **kwargs):
self.url = url
@ -86,6 +87,7 @@ class GitBackend(DataBackend):
widget=forms.TextInput(attrs={'class': 'form-control'})
)
}
sensitive_parameters = ['password']
@contextmanager
def fetch(self):
@ -135,6 +137,7 @@ class S3Backend(DataBackend):
widget=forms.TextInput(attrs={'class': 'form-control'})
),
}
sensitive_parameters = ['aws_secret_access_key']
REGION_REGEX = r's3\.([a-z0-9-]+)\.amazonaws\.com'

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from core.choices import DataSourceTypeChoices
from core.models import *
@ -15,6 +15,7 @@ __all__ = (
class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(DataSourceTypeChoices),
required=False,
initial=''
@ -25,16 +26,17 @@ class DataSourceBulkEditForm(NetBoxModelBulkEditForm):
label=_('Enforce unique space')
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
comments = CommentField(
label=_('Comments')
)
comments = CommentField()
parameters = forms.JSONField(
label=_('Parameters'),
required=False
)
ignore_rules = forms.CharField(
label=_('Ignore rules'),
required=False,
widget=forms.Textarea()
)

View File

@ -1,7 +1,7 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from core.choices import *
from core.models import *
@ -23,17 +23,20 @@ class DataSourceFilterForm(NetBoxModelFilterSetForm):
model = DataSource
fieldsets = (
(None, ('q', 'filter_id')),
('Data Source', ('type', 'status')),
(_('Data Source'), ('type', 'status')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=DataSourceTypeChoices,
required=False
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=DataSourceStatusChoices,
required=False
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
@ -45,7 +48,7 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
model = DataFile
fieldsets = (
(None, ('q', 'filter_id')),
('File', ('source_id',)),
(_('File'), ('source_id',)),
)
source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -57,8 +60,8 @@ class DataFileFilterForm(NetBoxModelFilterSetForm):
class JobFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', ('object_type', 'status')),
('Creation', (
(_('Attributes'), ('object_type', 'status')),
(_('Creation'), (
'created__before', 'created__after', 'scheduled__before', 'scheduled__after', 'started__before',
'started__after', 'completed__before', 'completed__after', 'user',
)),
@ -69,38 +72,47 @@ class JobFilterForm(SavedFiltersMixin, FilterForm):
required=False,
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=JobStatusChoices,
required=False
)
created__after = forms.DateTimeField(
label=_('Created after'),
required=False,
widget=DateTimePicker()
)
created__before = forms.DateTimeField(
label=_('Created before'),
required=False,
widget=DateTimePicker()
)
scheduled__after = forms.DateTimeField(
label=_('Scheduled after'),
required=False,
widget=DateTimePicker()
)
scheduled__before = forms.DateTimeField(
label=_('Scheduled before'),
required=False,
widget=DateTimePicker()
)
started__after = forms.DateTimeField(
label=_('Started after'),
required=False,
widget=DateTimePicker()
)
started__before = forms.DateTimeField(
label=_('Started before'),
required=False,
widget=DateTimePicker()
)
completed__after = forms.DateTimeField(
label=_('Completed after'),
required=False,
widget=DateTimePicker()
)
completed__before = forms.DateTimeField(
label=_('Completed before'),
required=False,
widget=DateTimePicker()
)

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from core.models import DataFile, DataSource
from utilities.forms.fields import DynamicModelChoiceField

View File

@ -1,6 +1,7 @@
import copy
from django import forms
from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from core.models import *
@ -38,11 +39,11 @@ class DataSourceForm(NetBoxModelForm):
@property
def fieldsets(self):
fieldsets = [
('Source', ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')),
(_('Source'), ('name', 'type', 'source_url', 'enabled', 'description', 'tags', 'ignore_rules')),
]
if self.backend_fields:
fieldsets.append(
('Backend Parameters', self.backend_fields)
(_('Backend Parameters'), self.backend_fields)
)
return fieldsets
@ -79,8 +80,8 @@ class ManagedFileForm(SyncedDataMixin, NetBoxModelForm):
)
fieldsets = (
('File Upload', ('upload_file',)),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
(_('File Upload'), ('upload_file',)),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
)
class Meta:

View File

@ -39,10 +39,12 @@ class DataSource(JobsMixin, PrimaryModel):
A remote source, such as a git repository, from which DataFiles are synchronized.
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True
)
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=DataSourceTypeChoices,
default=DataSourceTypeChoices.LOCAL
@ -52,23 +54,28 @@ class DataSource(JobsMixin, PrimaryModel):
verbose_name=_('URL')
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=DataSourceStatusChoices,
default=DataSourceStatusChoices.NEW,
editable=False
)
enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True
)
ignore_rules = models.TextField(
verbose_name=_('ignore rules'),
blank=True,
help_text=_("Patterns (one per line) matching files to ignore when syncing")
)
parameters = models.JSONField(
verbose_name=_('parameters'),
blank=True,
null=True
)
last_synced = models.DateTimeField(
verbose_name=_('last synced'),
blank=True,
null=True,
editable=False
@ -239,9 +246,11 @@ class DataFile(models.Model):
updated, or deleted only by calling DataSource.sync().
"""
created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True
)
last_updated = models.DateTimeField(
verbose_name=_('last updated'),
editable=False
)
source = models.ForeignKey(
@ -251,20 +260,23 @@ class DataFile(models.Model):
editable=False
)
path = models.CharField(
verbose_name=_('path'),
max_length=1000,
editable=False,
help_text=_("File path relative to the data source's root")
)
size = models.PositiveIntegerField(
editable=False
editable=False,
verbose_name=_('size')
)
hash = models.CharField(
verbose_name=_('hash'),
max_length=64,
editable=False,
validators=[
RegexValidator(regex='^[0-9a-f]{64}$', message=_("Length must be 64 hexadecimal characters."))
],
help_text=_("SHA256 hash of the file data")
help_text=_('SHA256 hash of the file data')
)
data = models.BinaryField()

View File

@ -23,20 +23,24 @@ class ManagedFile(SyncedDataMixin, models.Model):
to provide additional functionality.
"""
created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True
)
last_updated = models.DateTimeField(
verbose_name=_('last updated'),
editable=False,
blank=True,
null=True
)
file_root = models.CharField(
verbose_name=_('file root'),
max_length=1000,
choices=ManagedFileRootPathChoices
)
file_path = models.FilePathField(
verbose_name=_('file path'),
editable=False,
help_text=_("File path relative to the designated root path")
help_text=_('File path relative to the designated root path')
)
objects = RestrictedQuerySet.as_manager()

View File

@ -43,28 +43,34 @@ class Job(models.Model):
for_concrete_model=False
)
name = models.CharField(
verbose_name=_('name'),
max_length=200
)
created = models.DateTimeField(
verbose_name=_('created'),
auto_now_add=True
)
scheduled = models.DateTimeField(
verbose_name=_('scheduled'),
null=True,
blank=True
)
interval = models.PositiveIntegerField(
verbose_name=_('interval'),
blank=True,
null=True,
validators=(
MinValueValidator(1),
),
help_text=_("Recurrence interval (in minutes)")
help_text=_('Recurrence interval (in minutes)')
)
started = models.DateTimeField(
verbose_name=_('started'),
null=True,
blank=True
)
completed = models.DateTimeField(
verbose_name=_('completed'),
null=True,
blank=True
)
@ -76,15 +82,18 @@ class Job(models.Model):
null=True
)
status = models.CharField(
verbose_name=_('status'),
max_length=30,
choices=JobStatusChoices,
default=JobStatusChoices.STATUS_PENDING
)
data = models.JSONField(
verbose_name=_('data'),
null=True,
blank=True
)
job_id = models.UUIDField(
verbose_name=_('job ID'),
unique=True
)

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from core.models import *
@ -11,11 +12,18 @@ __all__ = (
class DataSourceTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
type = columns.ChoiceFieldColumn()
status = columns.ChoiceFieldColumn()
enabled = columns.BooleanColumn()
type = columns.ChoiceFieldColumn(
verbose_name=_('Type'),
)
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
enabled = columns.BooleanColumn(
verbose_name=_('Enabled'),
)
tags = columns.TagColumn(
url_name='core:datasource_list'
)
@ -34,12 +42,16 @@ class DataSourceTable(NetBoxTable):
class DataFileTable(NetBoxTable):
source = tables.Column(
verbose_name=_('Source'),
linkify=True
)
path = tables.Column(
verbose_name=_('Path'),
linkify=True
)
last_updated = columns.DateTimeColumn()
last_updated = columns.DateTimeColumn(
verbose_name=_('Last updated'),
)
actions = columns.ActionsColumn(
actions=('delete',)
)

View File

@ -1,5 +1,5 @@
import django_tables2 as tables
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from netbox.tables import NetBoxTable, columns
from ..models import Job
@ -7,23 +7,38 @@ from ..models import Job
class JobTable(NetBoxTable):
id = tables.Column(
verbose_name=_('ID'),
linkify=True
)
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
object_type = columns.ContentTypeColumn(
verbose_name=_('Type')
)
object = tables.Column(
verbose_name=_('Object'),
linkify=True
)
status = columns.ChoiceFieldColumn()
created = columns.DateTimeColumn()
scheduled = columns.DateTimeColumn()
interval = columns.DurationColumn()
started = columns.DateTimeColumn()
completed = columns.DateTimeColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
created = columns.DateTimeColumn(
verbose_name=_('Created'),
)
scheduled = columns.DateTimeColumn(
verbose_name=_('Scheduled'),
)
interval = columns.DurationColumn(
verbose_name=_('Interval'),
)
started = columns.DateTimeColumn(
verbose_name=_('Started'),
)
completed = columns.DateTimeColumn(
verbose_name=_('Completed'),
)
actions = columns.ActionsColumn(
actions=('delete',)
)

View File

@ -327,12 +327,28 @@ class DeviceTypeSerializer(NetBoxModelSerializer):
weight_unit = ChoiceField(choices=WeightUnitChoices, allow_blank=True, required=False, allow_null=True)
device_count = serializers.IntegerField(read_only=True)
# Counter fields
console_port_template_count = serializers.IntegerField(read_only=True)
console_server_port_template_count = serializers.IntegerField(read_only=True)
power_port_template_count = serializers.IntegerField(read_only=True)
power_outlet_template_count = serializers.IntegerField(read_only=True)
interface_template_count = serializers.IntegerField(read_only=True)
front_port_template_count = serializers.IntegerField(read_only=True)
rear_port_template_count = serializers.IntegerField(read_only=True)
device_bay_template_count = serializers.IntegerField(read_only=True)
module_bay_template_count = serializers.IntegerField(read_only=True)
inventory_item_template_count = serializers.IntegerField(read_only=True)
class Meta:
model = DeviceType
fields = [
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'id', 'url', 'display', 'manufacturer', 'default_platform', 'model', 'slug', 'part_number', 'u_height',
'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit', 'front_image', 'rear_image',
'description', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', 'device_count',
'console_port_template_count', 'console_server_port_template_count', 'power_port_template_count',
'power_outlet_template_count', 'interface_template_count', 'front_port_template_count',
'rear_port_template_count', 'device_bay_template_count', 'module_bay_template_count',
'inventory_item_template_count',
]
@ -498,12 +514,18 @@ class InterfaceTemplateSerializer(ValidatedModelSerializer):
allow_blank=True,
allow_null=True
)
rf_role = ChoiceField(
choices=WirelessRoleChoices,
required=False,
allow_blank=True,
allow_null=True
)
class Meta:
model = InterfaceTemplate
fields = [
'id', 'url', 'display', 'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only',
'description', 'bridge', 'poe_mode', 'poe_type', 'created', 'last_updated',
'description', 'bridge', 'poe_mode', 'poe_type', 'rf_role', 'created', 'last_updated',
]
@ -663,20 +685,35 @@ class DeviceSerializer(NetBoxModelSerializer):
primary_ip = NestedIPAddressSerializer(read_only=True)
primary_ip4 = NestedIPAddressSerializer(required=False, allow_null=True)
primary_ip6 = NestedIPAddressSerializer(required=False, allow_null=True)
oob_ip = NestedIPAddressSerializer(required=False, allow_null=True)
parent_device = serializers.SerializerMethodField()
cluster = NestedClusterSerializer(required=False, allow_null=True)
virtual_chassis = NestedVirtualChassisSerializer(required=False, allow_null=True, default=None)
vc_position = serializers.IntegerField(allow_null=True, max_value=255, min_value=0, default=None)
config_template = NestedConfigTemplateSerializer(required=False, allow_null=True, default=None)
# Counter fields
console_port_count = serializers.IntegerField(read_only=True)
console_server_port_count = serializers.IntegerField(read_only=True)
power_port_count = serializers.IntegerField(read_only=True)
power_outlet_count = serializers.IntegerField(read_only=True)
interface_count = serializers.IntegerField(read_only=True)
front_port_count = serializers.IntegerField(read_only=True)
rear_port_count = serializers.IntegerField(read_only=True)
device_bay_count = serializers.IntegerField(read_only=True)
module_bay_count = serializers.IntegerField(read_only=True)
inventory_item_count = serializers.IntegerField(read_only=True)
class Meta:
model = Device
fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status', 'airflow',
'primary_ip', 'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
'description', 'comments', 'config_template', 'local_context_data', 'tags', 'custom_fields', 'created',
'last_updated',
'site', 'location', 'rack', 'position', 'face', 'latitude', 'longitude', 'parent_device', 'status',
'airflow', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis',
'vc_position', 'vc_priority', 'description', 'comments', 'config_template', 'local_context_data', 'tags',
'custom_fields', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
'device_bay_count', 'module_bay_count', 'inventory_item_count',
]
@extend_schema_field(NestedDeviceSerializer)
@ -698,9 +735,11 @@ class DeviceWithConfigContextSerializer(DeviceSerializer):
fields = [
'id', 'url', 'display', 'name', 'device_type', 'device_role', 'tenant', 'platform', 'serial', 'asset_tag',
'site', 'location', 'rack', 'position', 'face', 'parent_device', 'status', 'airflow', 'primary_ip',
'primary_ip4', 'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context', 'config_template',
'created', 'last_updated',
'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority',
'description', 'comments', 'local_context_data', 'tags', 'custom_fields', 'config_context',
'config_template', 'created', 'last_updated', 'console_port_count', 'console_server_port_count',
'power_port_count', 'power_outlet_count', 'interface_count', 'front_port_count', 'rear_port_count',
'device_bay_count', 'module_bay_count', 'inventory_item_count',
]
@extend_schema_field(serializers.JSONField(allow_null=True))
@ -1139,13 +1178,15 @@ class CablePathSerializer(serializers.ModelSerializer):
class VirtualChassisSerializer(NetBoxModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False, allow_null=True, default=None)
# Counter fields
member_count = serializers.IntegerField(read_only=True)
class Meta:
model = VirtualChassis
fields = [
'id', 'url', 'display', 'name', 'domain', 'master', 'description', 'comments', 'tags', 'custom_fields',
'member_count', 'created', 'last_updated',
'created', 'last_updated', 'member_count',
]
@ -1195,6 +1236,10 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
choices=PowerFeedPhaseChoices,
default=lambda: PowerFeedPhaseChoices.PHASE_SINGLE,
)
tenant = NestedTenantSerializer(
required=False,
allow_null=True
)
class Meta:
model = PowerFeed
@ -1202,5 +1247,5 @@ class PowerFeedSerializer(NetBoxModelSerializer, CabledObjectSerializer, Connect
'id', 'url', 'display', 'power_panel', 'rack', 'name', 'status', 'type', 'supply', 'phase', 'voltage',
'amperage', 'max_utilization', 'mark_connected', 'cable', 'cable_end', 'link_peers', 'link_peers_type',
'connected_endpoints', 'connected_endpoints_type', 'connected_endpoints_reachable', 'description',
'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
'tenant', 'comments', 'tags', 'custom_fields', 'created', 'last_updated', '_occupied',
]

View File

@ -579,9 +579,7 @@ class CableTerminationViewSet(NetBoxModelViewSet):
#
class VirtualChassisViewSet(NetBoxModelViewSet):
queryset = VirtualChassis.objects.prefetch_related('tags').annotate(
member_count=count_related(Device, 'virtual_chassis')
)
queryset = VirtualChassis.objects.prefetch_related('tags')
serializer_class = serializers.VirtualChassisSerializer
filterset_class = filtersets.VirtualChassisFilterSet
brief_prefetch_fields = ['master']

View File

@ -9,7 +9,8 @@ class DCIMConfig(AppConfig):
def ready(self):
from . import signals, search
from .models import CableTermination
from .models import CableTermination, Device, DeviceType, VirtualChassis
from utilities.counters import connect_counters
# Register denormalized fields
denormalized.register(CableTermination, '_device', {
@ -24,3 +25,6 @@ class DCIMConfig(AppConfig):
denormalized.register(CableTermination, '_location', {
'_site': 'site',
})
# Register counters
connect_counters(Device, DeviceType, VirtualChassis)

View File

@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ChoiceSet
@ -15,11 +17,11 @@ class SiteStatusChoices(ChoiceSet):
STATUS_RETIRED = 'retired'
CHOICES = [
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_STAGING, 'Staging', 'blue'),
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
(STATUS_RETIRED, 'Retired', 'red'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_STAGING, _('Staging'), 'blue'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
(STATUS_RETIRED, _('Retired'), 'red'),
]
@ -60,13 +62,13 @@ class RackTypeChoices(ChoiceSet):
TYPE_WALLCABINET_VERTICAL = 'wall-cabinet-vertical'
CHOICES = (
(TYPE_2POST, '2-post frame'),
(TYPE_4POST, '4-post frame'),
(TYPE_CABINET, '4-post cabinet'),
(TYPE_WALLFRAME, 'Wall-mounted frame'),
(TYPE_WALLFRAME_VERTICAL, 'Wall-mounted frame (vertical)'),
(TYPE_WALLCABINET, 'Wall-mounted cabinet'),
(TYPE_WALLCABINET_VERTICAL, 'Wall-mounted cabinet (vertical)'),
(TYPE_2POST, _('2-post frame')),
(TYPE_4POST, _('4-post frame')),
(TYPE_CABINET, _('4-post cabinet')),
(TYPE_WALLFRAME, _('Wall-mounted frame')),
(TYPE_WALLFRAME_VERTICAL, _('Wall-mounted frame (vertical)')),
(TYPE_WALLCABINET, _('Wall-mounted cabinet')),
(TYPE_WALLCABINET_VERTICAL, _('Wall-mounted cabinet (vertical)')),
)
@ -78,10 +80,10 @@ class RackWidthChoices(ChoiceSet):
WIDTH_23IN = 23
CHOICES = (
(WIDTH_10IN, '10 inches'),
(WIDTH_19IN, '19 inches'),
(WIDTH_21IN, '21 inches'),
(WIDTH_23IN, '23 inches'),
(WIDTH_10IN, _('10 inches')),
(WIDTH_19IN, _('19 inches')),
(WIDTH_21IN, _('21 inches')),
(WIDTH_23IN, _('23 inches')),
)
@ -95,11 +97,11 @@ class RackStatusChoices(ChoiceSet):
STATUS_DEPRECATED = 'deprecated'
CHOICES = [
(STATUS_RESERVED, 'Reserved', 'yellow'),
(STATUS_AVAILABLE, 'Available', 'green'),
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_ACTIVE, 'Active', 'blue'),
(STATUS_DEPRECATED, 'Deprecated', 'red'),
(STATUS_RESERVED, _('Reserved'), 'yellow'),
(STATUS_AVAILABLE, _('Available'), 'green'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_ACTIVE, _('Active'), 'blue'),
(STATUS_DEPRECATED, _('Deprecated'), 'red'),
]
@ -109,8 +111,8 @@ class RackDimensionUnitChoices(ChoiceSet):
UNIT_INCH = 'in'
CHOICES = (
(UNIT_MILLIMETER, 'Millimeters'),
(UNIT_INCH, 'Inches'),
(UNIT_MILLIMETER, _('Millimeters')),
(UNIT_INCH, _('Inches')),
)
@ -135,8 +137,8 @@ class SubdeviceRoleChoices(ChoiceSet):
ROLE_CHILD = 'child'
CHOICES = (
(ROLE_PARENT, 'Parent'),
(ROLE_CHILD, 'Child'),
(ROLE_PARENT, _('Parent')),
(ROLE_CHILD, _('Child')),
)
@ -150,8 +152,8 @@ class DeviceFaceChoices(ChoiceSet):
FACE_REAR = 'rear'
CHOICES = (
(FACE_FRONT, 'Front'),
(FACE_REAR, 'Rear'),
(FACE_FRONT, _('Front')),
(FACE_REAR, _('Rear')),
)
@ -167,13 +169,13 @@ class DeviceStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = [
(STATUS_OFFLINE, 'Offline', 'gray'),
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_STAGED, 'Staged', 'blue'),
(STATUS_FAILED, 'Failed', 'red'),
(STATUS_INVENTORY, 'Inventory', 'purple'),
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
(STATUS_OFFLINE, _('Offline'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_STAGED, _('Staged'), 'blue'),
(STATUS_FAILED, _('Failed'), 'red'),
(STATUS_INVENTORY, _('Inventory'), 'purple'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
]
@ -188,13 +190,13 @@ class DeviceAirflowChoices(ChoiceSet):
AIRFLOW_MIXED = 'mixed'
CHOICES = (
(AIRFLOW_FRONT_TO_REAR, 'Front to rear'),
(AIRFLOW_REAR_TO_FRONT, 'Rear to front'),
(AIRFLOW_LEFT_TO_RIGHT, 'Left to right'),
(AIRFLOW_RIGHT_TO_LEFT, 'Right to left'),
(AIRFLOW_SIDE_TO_REAR, 'Side to rear'),
(AIRFLOW_PASSIVE, 'Passive'),
(AIRFLOW_MIXED, 'Mixed'),
(AIRFLOW_FRONT_TO_REAR, _('Front to rear')),
(AIRFLOW_REAR_TO_FRONT, _('Rear to front')),
(AIRFLOW_LEFT_TO_RIGHT, _('Left to right')),
(AIRFLOW_RIGHT_TO_LEFT, _('Right to left')),
(AIRFLOW_SIDE_TO_REAR, _('Side to rear')),
(AIRFLOW_PASSIVE, _('Passive')),
(AIRFLOW_MIXED, _('Mixed')),
)
@ -213,12 +215,12 @@ class ModuleStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = [
(STATUS_OFFLINE, 'Offline', 'gray'),
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_STAGED, 'Staged', 'blue'),
(STATUS_FAILED, 'Failed', 'red'),
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
(STATUS_OFFLINE, _('Offline'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_STAGED, _('Staged'), 'blue'),
(STATUS_FAILED, _('Failed'), 'red'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
]
@ -318,6 +320,10 @@ class PowerPortTypeChoices(ChoiceSet):
TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h'
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# IEC 60906-1
TYPE_IEC_60906_1 = 'iec-60906-1'
TYPE_NBR_14136_10A = 'nbr-14136-10a'
TYPE_NBR_14136_20A = 'nbr-14136-20a'
# NEMA non-locking
TYPE_NEMA_115P = 'nema-1-15p'
TYPE_NEMA_515P = 'nema-5-15p'
@ -429,7 +435,12 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE6H, '3P+N+E 6H'),
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)),
('NEMA (Non-locking)', (
('IEC 60906-1', (
(TYPE_IEC_60906_1, 'IEC 60906-1'),
(TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'),
(TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'),
)),
(_('NEMA (Non-locking)'), (
(TYPE_NEMA_115P, 'NEMA 1-15P'),
(TYPE_NEMA_515P, 'NEMA 5-15P'),
(TYPE_NEMA_520P, 'NEMA 5-20P'),
@ -451,7 +462,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_1550P, 'NEMA 15-50P'),
(TYPE_NEMA_1560P, 'NEMA 15-60P'),
)),
('NEMA (Locking)', (
(_('NEMA (Locking)'), (
(TYPE_NEMA_L115P, 'NEMA L1-15P'),
(TYPE_NEMA_L515P, 'NEMA L5-15P'),
(TYPE_NEMA_L520P, 'NEMA L5-20P'),
@ -474,7 +485,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEMA_L2130P, 'NEMA L21-30P'),
(TYPE_NEMA_L2230P, 'NEMA L22-30P'),
)),
('California Style', (
(_('California Style'), (
(TYPE_CS6361C, 'CS6361C'),
(TYPE_CS6365C, 'CS6365C'),
(TYPE_CS8165C, 'CS8165C'),
@ -482,7 +493,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_CS8365C, 'CS8365C'),
(TYPE_CS8465C, 'CS8465C'),
)),
('International/ITA', (
(_('International/ITA'), (
(TYPE_ITA_C, 'ITA Type C (CEE 7/16)'),
(TYPE_ITA_E, 'ITA Type E (CEE 7/6)'),
(TYPE_ITA_F, 'ITA Type F (CEE 7/4)'),
@ -512,7 +523,7 @@ class PowerPortTypeChoices(ChoiceSet):
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', (
(_('Proprietary'), (
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
(TYPE_NEUTRIK_POWERCON_32A, 'Neutrik powerCON (32A)'),
@ -520,7 +531,7 @@ class PowerPortTypeChoices(ChoiceSet):
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
(TYPE_UBIQUITI_SMARTPOWER, 'Ubiquiti SmartPower'),
)),
('Other', (
(_('Other'), (
(TYPE_HARDWIRED, 'Hardwired'),
(TYPE_OTHER, 'Other'),
)),
@ -553,6 +564,10 @@ class PowerOutletTypeChoices(ChoiceSet):
TYPE_IEC_3PNE4H = 'iec-60309-3p-n-e-4h'
TYPE_IEC_3PNE6H = 'iec-60309-3p-n-e-6h'
TYPE_IEC_3PNE9H = 'iec-60309-3p-n-e-9h'
# IEC 60906-1
TYPE_IEC_60906_1 = 'iec-60906-1'
TYPE_NBR_14136_10A = 'nbr-14136-10a'
TYPE_NBR_14136_20A = 'nbr-14136-20a'
# NEMA non-locking
TYPE_NEMA_115R = 'nema-1-15r'
TYPE_NEMA_515R = 'nema-5-15r'
@ -657,7 +672,12 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_IEC_3PNE6H, '3P+N+E 6H'),
(TYPE_IEC_3PNE9H, '3P+N+E 9H'),
)),
('NEMA (Non-locking)', (
('IEC 60906-1', (
(TYPE_IEC_60906_1, 'IEC 60906-1'),
(TYPE_NBR_14136_10A, '2P+T 10A (NBR 14136)'),
(TYPE_NBR_14136_20A, '2P+T 20A (NBR 14136)'),
)),
(_('NEMA (Non-locking)'), (
(TYPE_NEMA_115R, 'NEMA 1-15R'),
(TYPE_NEMA_515R, 'NEMA 5-15R'),
(TYPE_NEMA_520R, 'NEMA 5-20R'),
@ -679,7 +699,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_1550R, 'NEMA 15-50R'),
(TYPE_NEMA_1560R, 'NEMA 15-60R'),
)),
('NEMA (Locking)', (
(_('NEMA (Locking)'), (
(TYPE_NEMA_L115R, 'NEMA L1-15R'),
(TYPE_NEMA_L515R, 'NEMA L5-15R'),
(TYPE_NEMA_L520R, 'NEMA L5-20R'),
@ -702,7 +722,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEMA_L2130R, 'NEMA L21-30R'),
(TYPE_NEMA_L2230R, 'NEMA L22-30R'),
)),
('California Style', (
(_('California Style'), (
(TYPE_CS6360C, 'CS6360C'),
(TYPE_CS6364C, 'CS6364C'),
(TYPE_CS8164C, 'CS8164C'),
@ -710,7 +730,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_CS8364C, 'CS8364C'),
(TYPE_CS8464C, 'CS8464C'),
)),
('ITA/International', (
(_('ITA/International'), (
(TYPE_ITA_E, 'ITA Type E (CEE 7/5)'),
(TYPE_ITA_F, 'ITA Type F (CEE 7/3)'),
(TYPE_ITA_G, 'ITA Type G (BS 1363)'),
@ -732,7 +752,7 @@ class PowerOutletTypeChoices(ChoiceSet):
('DC', (
(TYPE_DC, 'DC Terminal'),
)),
('Proprietary', (
(_('Proprietary'), (
(TYPE_HDOT_CX, 'HDOT Cx'),
(TYPE_SAF_D_GRID, 'Saf-D-Grid'),
(TYPE_NEUTRIK_POWERCON_20A, 'Neutrik powerCON (20A)'),
@ -741,7 +761,7 @@ class PowerOutletTypeChoices(ChoiceSet):
(TYPE_NEUTRIK_POWERCON_TRUE1_TOP, 'Neutrik powerCON TRUE1 TOP'),
(TYPE_UBIQUITI_SMARTPOWER, 'Ubiquiti SmartPower'),
)),
('Other', (
(_('Other'), (
(TYPE_HARDWIRED, 'Hardwired'),
(TYPE_OTHER, 'Other'),
)),
@ -771,9 +791,9 @@ class InterfaceKindChoices(ChoiceSet):
KIND_WIRELESS = 'wireless'
CHOICES = (
(KIND_PHYSICAL, 'Physical'),
(KIND_VIRTUAL, 'Virtual'),
(KIND_WIRELESS, 'Wireless'),
(KIND_PHYSICAL, _('Physical')),
(KIND_VIRTUAL, _('Virtual')),
(KIND_WIRELESS, _('Wireless')),
)
@ -809,6 +829,8 @@ class InterfaceTypeChoices(ChoiceSet):
TYPE_100GE_CFP4 = '100gbase-x-cfp4'
TYPE_100GE_CXP = '100gbase-x-cxp'
TYPE_100GE_CPAK = '100gbase-x-cpak'
TYPE_100GE_DSFP = '100gbase-x-dsfp'
TYPE_100GE_SFP_DD = '100gbase-x-sfpdd'
TYPE_100GE_QSFP28 = '100gbase-x-qsfp28'
TYPE_100GE_QSFP_DD = '100gbase-x-qsfpdd'
TYPE_200GE_CFP2 = '200gbase-x-cfp2'
@ -919,15 +941,15 @@ class InterfaceTypeChoices(ChoiceSet):
CHOICES = (
(
'Virtual interfaces',
_('Virtual interfaces'),
(
(TYPE_VIRTUAL, 'Virtual'),
(TYPE_BRIDGE, 'Bridge'),
(TYPE_LAG, 'Link Aggregation Group (LAG)'),
(TYPE_VIRTUAL, _('Virtual')),
(TYPE_BRIDGE, _('Bridge')),
(TYPE_LAG, _('Link Aggregation Group (LAG)')),
),
),
(
'Ethernet (fixed)',
_('Ethernet (fixed)'),
(
(TYPE_100ME_FX, '100BASE-FX (10/100ME FIBER)'),
(TYPE_100ME_LFX, '100BASE-LFX (10/100ME FIBER)'),
@ -941,7 +963,7 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Ethernet (modular)',
_('Ethernet (modular)'),
(
(TYPE_1GE_GBIC, 'GBIC (1GE)'),
(TYPE_1GE_SFP, 'SFP (1GE)'),
@ -959,6 +981,8 @@ class InterfaceTypeChoices(ChoiceSet):
(TYPE_100GE_CFP4, 'CFP4 (100GE)'),
(TYPE_100GE_CXP, 'CXP (100GE)'),
(TYPE_100GE_CPAK, 'Cisco CPAK (100GE)'),
(TYPE_100GE_DSFP, 'DSFP (100GE)'),
(TYPE_100GE_SFP_DD, 'SFP-DD (100GE)'),
(TYPE_100GE_QSFP28, 'QSFP28 (100GE)'),
(TYPE_100GE_QSFP_DD, 'QSFP-DD (100GE)'),
(TYPE_200GE_QSFP56, 'QSFP56 (200GE)'),
@ -972,7 +996,7 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Ethernet (backplane)',
_('Ethernet (backplane)'),
(
(TYPE_1GE_KX, '1000BASE-KX (1GE)'),
(TYPE_10GE_KR, '10GBASE-KR (10GE)'),
@ -986,7 +1010,7 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Wireless',
_('Wireless'),
(
(TYPE_80211A, 'IEEE 802.11a'),
(TYPE_80211G, 'IEEE 802.11b/g'),
@ -1000,7 +1024,7 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Cellular',
_('Cellular'),
(
(TYPE_GSM, 'GSM'),
(TYPE_CDMA, 'CDMA'),
@ -1047,7 +1071,7 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Serial',
_('Serial'),
(
(TYPE_T1, 'T1 (1.544 Mbps)'),
(TYPE_E1, 'E1 (2.048 Mbps)'),
@ -1062,7 +1086,7 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Coaxial',
_('Coaxial'),
(
(TYPE_DOCSIS, 'DOCSIS'),
)
@ -1079,7 +1103,7 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Stacking',
_('Stacking'),
(
(TYPE_STACKWISE, 'Cisco StackWise'),
(TYPE_STACKWISE_PLUS, 'Cisco StackWise Plus'),
@ -1098,9 +1122,9 @@ class InterfaceTypeChoices(ChoiceSet):
)
),
(
'Other',
_('Other'),
(
(TYPE_OTHER, 'Other'),
(TYPE_OTHER, _('Other')),
)
),
)
@ -1127,9 +1151,9 @@ class InterfaceDuplexChoices(ChoiceSet):
DUPLEX_AUTO = 'auto'
CHOICES = (
(DUPLEX_HALF, 'Half'),
(DUPLEX_FULL, 'Full'),
(DUPLEX_AUTO, 'Auto'),
(DUPLEX_HALF, _('Half')),
(DUPLEX_FULL, _('Full')),
(DUPLEX_AUTO, _('Auto')),
)
@ -1140,9 +1164,9 @@ class InterfaceModeChoices(ChoiceSet):
MODE_TAGGED_ALL = 'tagged-all'
CHOICES = (
(MODE_ACCESS, 'Access'),
(MODE_TAGGED, 'Tagged'),
(MODE_TAGGED_ALL, 'Tagged (All)'),
(MODE_ACCESS, _('Access')),
(MODE_TAGGED, _('Tagged')),
(MODE_TAGGED_ALL, _('Tagged (All)')),
)
@ -1171,7 +1195,7 @@ class InterfacePoETypeChoices(ChoiceSet):
CHOICES = (
(
'IEEE Standard',
_('IEEE Standard'),
(
(TYPE_1_8023AF, '802.3af (Type 1)'),
(TYPE_2_8023AT, '802.3at (Type 2)'),
@ -1180,12 +1204,12 @@ class InterfacePoETypeChoices(ChoiceSet):
)
),
(
'Passive',
_('Passive'),
(
(PASSIVE_24V_2PAIR, 'Passive 24V (2-pair)'),
(PASSIVE_24V_4PAIR, 'Passive 24V (4-pair)'),
(PASSIVE_48V_2PAIR, 'Passive 48V (2-pair)'),
(PASSIVE_48V_4PAIR, 'Passive 48V (4-pair)'),
(PASSIVE_24V_2PAIR, _('Passive 24V (2-pair)')),
(PASSIVE_24V_4PAIR, _('Passive 24V (4-pair)')),
(PASSIVE_48V_2PAIR, _('Passive 48V (2-pair)')),
(PASSIVE_48V_4PAIR, _('Passive 48V (4-pair)')),
)
),
)
@ -1247,7 +1271,7 @@ class PortTypeChoices(ChoiceSet):
CHOICES = (
(
'Copper',
_('Copper'),
(
(TYPE_8P8C, '8P8C'),
(TYPE_8P6C, '8P6C'),
@ -1270,7 +1294,7 @@ class PortTypeChoices(ChoiceSet):
),
),
(
'Fiber Optic',
_('Fiber Optic'),
(
(TYPE_FC, 'FC'),
(TYPE_LC, 'LC'),
@ -1303,9 +1327,9 @@ class PortTypeChoices(ChoiceSet):
),
),
(
'Other',
_('Other'),
(
(TYPE_OTHER, 'Other'),
(TYPE_OTHER, _('Other')),
)
)
)
@ -1343,7 +1367,7 @@ class CableTypeChoices(ChoiceSet):
CHOICES = (
(
'Copper', (
_('Copper'), (
(TYPE_CAT3, 'CAT3'),
(TYPE_CAT5, 'CAT5'),
(TYPE_CAT5E, 'CAT5e'),
@ -1359,7 +1383,7 @@ class CableTypeChoices(ChoiceSet):
),
),
(
'Fiber', (
_('Fiber'), (
(TYPE_MMF, 'Multimode Fiber'),
(TYPE_MMF_OM1, 'Multimode Fiber (OM1)'),
(TYPE_MMF_OM2, 'Multimode Fiber (OM2)'),
@ -1372,7 +1396,7 @@ class CableTypeChoices(ChoiceSet):
(TYPE_AOC, 'Active Optical Cabling (AOC)'),
),
),
(TYPE_POWER, 'Power'),
(TYPE_POWER, _('Power')),
)
@ -1383,9 +1407,9 @@ class LinkStatusChoices(ChoiceSet):
STATUS_DECOMMISSIONING = 'decommissioning'
CHOICES = (
(STATUS_CONNECTED, 'Connected', 'green'),
(STATUS_PLANNED, 'Planned', 'blue'),
(STATUS_DECOMMISSIONING, 'Decommissioning', 'yellow'),
(STATUS_CONNECTED, _('Connected'), 'green'),
(STATUS_PLANNED, _('Planned'), 'blue'),
(STATUS_DECOMMISSIONING, _('Decommissioning'), 'yellow'),
)
@ -1402,12 +1426,12 @@ class CableLengthUnitChoices(ChoiceSet):
UNIT_INCH = 'in'
CHOICES = (
(UNIT_KILOMETER, 'Kilometers'),
(UNIT_METER, 'Meters'),
(UNIT_CENTIMETER, 'Centimeters'),
(UNIT_MILE, 'Miles'),
(UNIT_FOOT, 'Feet'),
(UNIT_INCH, 'Inches'),
(UNIT_KILOMETER, _('Kilometers')),
(UNIT_METER, _('Meters')),
(UNIT_CENTIMETER, _('Centimeters')),
(UNIT_MILE, _('Miles')),
(UNIT_FOOT, _('Feet')),
(UNIT_INCH, _('Inches')),
)
@ -1422,10 +1446,10 @@ class WeightUnitChoices(ChoiceSet):
UNIT_OUNCE = 'oz'
CHOICES = (
(UNIT_KILOGRAM, 'Kilograms'),
(UNIT_GRAM, 'Grams'),
(UNIT_POUND, 'Pounds'),
(UNIT_OUNCE, 'Ounces'),
(UNIT_KILOGRAM, _('Kilograms')),
(UNIT_GRAM, _('Grams')),
(UNIT_POUND, _('Pounds')),
(UNIT_OUNCE, _('Ounces')),
)
@ -1458,10 +1482,10 @@ class PowerFeedStatusChoices(ChoiceSet):
STATUS_FAILED = 'failed'
CHOICES = [
(STATUS_OFFLINE, 'Offline', 'gray'),
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_PLANNED, 'Planned', 'blue'),
(STATUS_FAILED, 'Failed', 'red'),
(STATUS_OFFLINE, _('Offline'), 'gray'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'blue'),
(STATUS_FAILED, _('Failed'), 'red'),
]
@ -1471,8 +1495,8 @@ class PowerFeedTypeChoices(ChoiceSet):
TYPE_REDUNDANT = 'redundant'
CHOICES = (
(TYPE_PRIMARY, 'Primary', 'green'),
(TYPE_REDUNDANT, 'Redundant', 'cyan'),
(TYPE_PRIMARY, _('Primary'), 'green'),
(TYPE_REDUNDANT, _('Redundant'), 'cyan'),
)
@ -1493,8 +1517,8 @@ class PowerFeedPhaseChoices(ChoiceSet):
PHASE_3PHASE = 'three-phase'
CHOICES = (
(PHASE_SINGLE, 'Single phase'),
(PHASE_3PHASE, 'Three-phase'),
(PHASE_SINGLE, _('Single phase')),
(PHASE_3PHASE, _('Three-phase')),
)
@ -1509,7 +1533,7 @@ class VirtualDeviceContextStatusChoices(ChoiceSet):
STATUS_OFFLINE = 'offline'
CHOICES = [
(STATUS_ACTIVE, 'Active', 'green'),
(STATUS_PLANNED, 'Planned', 'cyan'),
(STATUS_OFFLINE, 'Offline', 'red'),
(STATUS_ACTIVE, _('Active'), 'green'),
(STATUS_PLANNED, _('Planned'), 'cyan'),
(STATUS_OFFLINE, _('Offline'), 'red'),
]

View File

@ -6,7 +6,6 @@ from netaddr import AddrFormatError, EUI, eui64_unix_expanded, mac_unix_expanded
from .lookups import PathContains
__all__ = (
'ASNField',
'MACAddressField',
'PathField',
'WWNField',

View File

@ -696,6 +696,9 @@ class InterfaceTemplateFilterSet(ChangeLoggedModelFilterSet, ModularDeviceTypeCo
poe_type = django_filters.MultipleChoiceFilter(
choices=InterfacePoETypeChoices
)
rf_role = django_filters.MultipleChoiceFilter(
choices=WirelessRoleChoices
)
class Meta:
model = InterfaceTemplate
@ -941,6 +944,10 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
method='_has_primary_ip',
label=_('Has a primary IP'),
)
has_oob_ip = django_filters.BooleanFilter(
method='_has_oob_ip',
label=_('Has an out-of-band IP'),
)
virtual_chassis_id = django_filters.ModelMultipleChoiceFilter(
field_name='virtual_chassis',
queryset=VirtualChassis.objects.all(),
@ -996,6 +1003,11 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
queryset=IPAddress.objects.all(),
label=_('Primary IPv6 (ID)'),
)
oob_ip_id = django_filters.ModelMultipleChoiceFilter(
field_name='oob_ip',
queryset=IPAddress.objects.all(),
label=_('OOB IP (ID)'),
)
class Meta:
model = Device
@ -1020,6 +1032,12 @@ class DeviceFilterSet(NetBoxModelFilterSet, TenancyFilterSet, ContactModelFilter
return queryset.filter(params)
return queryset.exclude(params)
def _has_oob_ip(self, queryset, name, value):
params = Q(oob_ip__isnull=False)
if value:
return queryset.filter(params)
return queryset.exclude(params)
def _virtual_chassis_member(self, queryset, name, value):
return queryset.exclude(virtual_chassis__isnull=value)
@ -1862,7 +1880,7 @@ class PowerPanelFilterSet(NetBoxModelFilterSet, ContactModelFilterSet):
return queryset.filter(qs_filter)
class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet):
class PowerFeedFilterSet(NetBoxModelFilterSet, CabledObjectFilterSet, PathEndpointFilterSet, TenancyFilterSet):
region_id = TreeNodeMultipleChoiceFilter(
queryset=Region.objects.all(),
field_name='power_panel__site__region',

View File

@ -1,7 +1,7 @@
from django import forms
from dcim.models import *
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from extras.forms import CustomFieldsMixin
from extras.models import Tag
from utilities.forms import BootstrapMixin, form_from_model
@ -32,10 +32,12 @@ class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCre
widget=forms.MultipleHiddenInput()
)
description = forms.CharField(
label=_('Description'),
max_length=100,
required=False
)
tags = DynamicModelMultipleChoiceField(
label=_('Tags'),
queryset=Tag.objects.all(),
required=False
)
@ -76,14 +78,14 @@ class PowerOutletBulkCreateForm(
class InterfaceBulkCreateForm(
form_from_model(Interface, [
'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type',
'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'mark_connected', 'poe_mode', 'poe_type', 'rf_role'
]),
DeviceBulkAddComponentForm
):
model = Interface
field_order = (
'name', 'label', 'type', 'enabled', 'speed', 'duplex', 'mtu', 'mgmt_only', 'poe_mode',
'poe_type', 'mark_connected', 'description', 'tags',
'poe_type', 'mark_connected', 'rf_role', 'description', 'tags',
)

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms.array import SimpleArrayField
from django.core.exceptions import ObjectDoesNotExist
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@ -56,6 +56,7 @@ __all__ = (
class RegionImportForm(NetBoxModelImportForm):
parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Region.objects.all(),
required=False,
to_field_name='name',
@ -69,6 +70,7 @@ class RegionImportForm(NetBoxModelImportForm):
class SiteGroupImportForm(NetBoxModelImportForm):
parent = CSVModelChoiceField(
label=_('Parent'),
queryset=SiteGroup.objects.all(),
required=False,
to_field_name='name',
@ -82,22 +84,26 @@ class SiteGroupImportForm(NetBoxModelImportForm):
class SiteImportForm(NetBoxModelImportForm):
status = CSVChoiceField(
label=_('Status'),
choices=SiteStatusChoices,
help_text=_('Operational status')
)
region = CSVModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned region')
)
group = CSVModelChoiceField(
label=_('Group'),
queryset=SiteGroup.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned group')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
@ -119,11 +125,13 @@ class SiteImportForm(NetBoxModelImportForm):
class LocationImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
help_text=_('Assigned site')
)
parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Location.objects.all(),
required=False,
to_field_name='name',
@ -133,10 +141,12 @@ class LocationImportForm(NetBoxModelImportForm):
}
)
status = CSVChoiceField(
label=_('Status'),
choices=LocationStatusChoices,
help_text=_('Operational status')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
@ -161,45 +171,54 @@ class RackRoleImportForm(NetBoxModelImportForm):
class RackImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name'
)
location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
to_field_name='name'
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Name of assigned tenant')
)
status = CSVChoiceField(
label=_('Status'),
choices=RackStatusChoices,
help_text=_('Operational status')
)
role = CSVModelChoiceField(
label=_('Role'),
queryset=RackRole.objects.all(),
required=False,
to_field_name='name',
help_text=_('Name of assigned role')
)
type = CSVChoiceField(
label=_('Type'),
choices=RackTypeChoices,
required=False,
help_text=_('Rack type')
)
width = forms.ChoiceField(
label=_('Width'),
choices=RackWidthChoices,
help_text=_('Rail-to-rail width (in inches)')
)
outer_unit = CSVChoiceField(
label=_('Outer unit'),
choices=RackDimensionUnitChoices,
required=False,
help_text=_('Unit for outer dimensions')
)
weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices,
required=False,
help_text=_('Unit for rack weights')
@ -225,27 +244,32 @@ class RackImportForm(NetBoxModelImportForm):
class RackReservationImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
help_text=_('Parent site')
)
location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
to_field_name='name',
required=False,
help_text=_("Rack's location (if any)")
)
rack = CSVModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
to_field_name='name',
help_text=_('Rack')
)
units = SimpleArrayField(
label=_('Units'),
base_field=forms.IntegerField(),
required=True,
help_text=_('Comma-separated list of individual unit numbers')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
@ -282,21 +306,25 @@ class ManufacturerImportForm(NetBoxModelImportForm):
class DeviceTypeImportForm(NetBoxModelImportForm):
manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
to_field_name='name',
help_text=_('The manufacturer which produces this device type')
)
default_platform = forms.ModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(),
to_field_name='name',
required=False,
help_text=_('The default platform for devices of this type (optional)')
)
weight = forms.DecimalField(
label=_('Weight'),
required=False,
help_text=_('Device weight'),
)
weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices,
required=False,
help_text=_('Unit for device weight')
@ -312,14 +340,17 @@ class DeviceTypeImportForm(NetBoxModelImportForm):
class ModuleTypeImportForm(NetBoxModelImportForm):
manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
to_field_name='name'
)
weight = forms.DecimalField(
label=_('Weight'),
required=False,
help_text=_('Module weight'),
)
weight_unit = CSVChoiceField(
label=_('Weight unit'),
choices=WeightUnitChoices,
required=False,
help_text=_('Unit for module weight')
@ -332,6 +363,7 @@ class ModuleTypeImportForm(NetBoxModelImportForm):
class DeviceRoleImportForm(NetBoxModelImportForm):
config_template = CSVModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
to_field_name='name',
required=False,
@ -350,12 +382,14 @@ class DeviceRoleImportForm(NetBoxModelImportForm):
class PlatformImportForm(NetBoxModelImportForm):
slug = SlugField()
manufacturer = CSVModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
required=False,
to_field_name='name',
help_text=_('Limit platform assignments to this manufacturer')
)
config_template = CSVModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
to_field_name='name',
required=False,
@ -371,43 +405,51 @@ class PlatformImportForm(NetBoxModelImportForm):
class BaseDeviceImportForm(NetBoxModelImportForm):
device_role = CSVModelChoiceField(
label=_('Device role'),
queryset=DeviceRole.objects.all(),
to_field_name='name',
help_text=_('Assigned role')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned tenant')
)
manufacturer = CSVModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
to_field_name='name',
help_text=_('Device type manufacturer')
)
device_type = CSVModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
to_field_name='model',
help_text=_('Device type model')
)
platform = CSVModelChoiceField(
label=_('Platform'),
queryset=Platform.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned platform')
)
status = CSVChoiceField(
label=_('Status'),
choices=DeviceStatusChoices,
help_text=_('Operational status')
)
virtual_chassis = CSVModelChoiceField(
label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(),
to_field_name='name',
required=False,
help_text=_('Virtual chassis')
)
cluster = CSVModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(),
to_field_name='name',
required=False,
@ -430,45 +472,53 @@ class BaseDeviceImportForm(NetBoxModelImportForm):
class DeviceImportForm(BaseDeviceImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
help_text=_('Assigned site')
)
location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
to_field_name='name',
required=False,
help_text=_("Assigned location (if any)")
)
rack = CSVModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
to_field_name='name',
required=False,
help_text=_("Assigned rack (if any)")
)
face = CSVChoiceField(
label=_('Face'),
choices=DeviceFaceChoices,
required=False,
help_text=_('Mounted rack face')
)
parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Device.objects.all(),
to_field_name='name',
required=False,
help_text=_('Parent device (for child devices)')
)
device_bay = CSVModelChoiceField(
label=_('Device bay'),
queryset=DeviceBay.objects.all(),
to_field_name='name',
required=False,
help_text=_('Device bay in which this device is installed (for child devices)')
)
airflow = CSVChoiceField(
label=_('Airflow'),
choices=DeviceAirflowChoices,
required=False,
help_text=_('Airflow direction')
)
config_template = CSVModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
to_field_name='name',
required=False,
@ -523,29 +573,35 @@ class DeviceImportForm(BaseDeviceImportForm):
class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name',
help_text=_('The device in which this module is installed')
)
module_bay = CSVModelChoiceField(
label=_('Module bay'),
queryset=ModuleBay.objects.all(),
to_field_name='name',
help_text=_('The module bay in which this module is installed')
)
module_type = CSVModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
to_field_name='model',
help_text=_('The type of module')
)
status = CSVChoiceField(
label=_('Status'),
choices=ModuleStatusChoices,
help_text=_('Operational status')
)
replicate_components = forms.BooleanField(
label=_('Replicate components'),
required=False,
help_text=_('Automatically populate components associated with this module type (enabled by default)')
)
adopt_components = forms.BooleanField(
label=_('Adopt components'),
required=False,
help_text=_('Adopt already existing components')
)
@ -579,15 +635,18 @@ class ModuleImportForm(ModuleCommonForm, NetBoxModelImportForm):
class ConsolePortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices,
required=False,
help_text=_('Port type')
)
speed = CSVTypedChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices,
coerce=int,
empty_value=None,
@ -602,15 +661,18 @@ class ConsolePortImportForm(NetBoxModelImportForm):
class ConsoleServerPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices,
required=False,
help_text=_('Port type')
)
speed = CSVTypedChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices,
coerce=int,
empty_value=None,
@ -625,10 +687,12 @@ class ConsoleServerPortImportForm(NetBoxModelImportForm):
class PowerPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
choices=PowerPortTypeChoices,
required=False,
help_text=_('Port type')
@ -643,21 +707,25 @@ class PowerPortImportForm(NetBoxModelImportForm):
class PowerOutletImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
choices=PowerOutletTypeChoices,
required=False,
help_text=_('Outlet type')
)
power_port = CSVModelChoiceField(
label=_('Power port'),
queryset=PowerPort.objects.all(),
required=False,
to_field_name='name',
help_text=_('Local power port which feeds this outlet')
)
feed_leg = CSVChoiceField(
label=_('Feed lag'),
choices=PowerOutletFeedLegChoices,
required=False,
help_text=_('Electrical phase (for three-phase circuits)')
@ -692,63 +760,75 @@ class PowerOutletImportForm(NetBoxModelImportForm):
class InterfaceImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent interface')
)
bridge = CSVModelChoiceField(
label=_('Bridge'),
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text=_('Bridged interface')
)
lag = CSVModelChoiceField(
label=_('Lag'),
queryset=Interface.objects.all(),
required=False,
to_field_name='name',
help_text=_('Parent LAG interface')
)
vdcs = CSVModelMultipleChoiceField(
label=_('Vdcs'),
queryset=VirtualDeviceContext.objects.all(),
required=False,
to_field_name='name',
help_text='VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")'
help_text=_('VDC names separated by commas, encased with double quotes (e.g. "vdc1, vdc2, vdc3")')
)
type = CSVChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices,
help_text=_('Physical medium')
)
duplex = CSVChoiceField(
label=_('Duplex'),
choices=InterfaceDuplexChoices,
required=False
)
poe_mode = CSVChoiceField(
label=_('Poe mode'),
choices=InterfacePoEModeChoices,
required=False,
help_text=_('PoE mode')
)
poe_type = CSVChoiceField(
label=_('Poe type'),
choices=InterfacePoETypeChoices,
required=False,
help_text=_('PoE type')
)
mode = CSVChoiceField(
label=_('Mode'),
choices=InterfaceModeChoices,
required=False,
help_text=_('IEEE 802.1Q operational mode (for L2 interfaces)')
)
vrf = CSVModelChoiceField(
label=_('VRF'),
queryset=VRF.objects.all(),
required=False,
to_field_name='rd',
help_text=_('Assigned VRF')
)
rf_role = CSVChoiceField(
label=_('Rf role'),
choices=WirelessRoleChoices,
required=False,
help_text=_('Wireless role (AP/station)')
@ -792,15 +872,18 @@ class InterfaceImportForm(NetBoxModelImportForm):
class FrontPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
rear_port = CSVModelChoiceField(
label=_('Rear port'),
queryset=RearPort.objects.all(),
to_field_name='name',
help_text=_('Corresponding rear port')
)
type = CSVChoiceField(
label=_('Type'),
choices=PortTypeChoices,
help_text=_('Physical medium classification')
)
@ -837,10 +920,12 @@ class FrontPortImportForm(NetBoxModelImportForm):
class RearPortImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
type = CSVChoiceField(
label=_('Type'),
help_text=_('Physical medium classification'),
choices=PortTypeChoices,
)
@ -852,6 +937,7 @@ class RearPortImportForm(NetBoxModelImportForm):
class ModuleBayImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
@ -863,10 +949,12 @@ class ModuleBayImportForm(NetBoxModelImportForm):
class DeviceBayImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
installed_device = CSVModelChoiceField(
label=_('Installed device'),
queryset=Device.objects.all(),
required=False,
to_field_name='name',
@ -909,32 +997,38 @@ class DeviceBayImportForm(NetBoxModelImportForm):
class InventoryItemImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name'
)
role = CSVModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(),
to_field_name='name',
required=False
)
manufacturer = CSVModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
to_field_name='name',
required=False
)
parent = CSVModelChoiceField(
label=_('Parent'),
queryset=Device.objects.all(),
to_field_name='name',
required=False,
help_text=_('Parent inventory item')
)
component_type = CSVContentTypeField(
label=_('Component type'),
queryset=ContentType.objects.all(),
limit_choices_to=MODULAR_COMPONENT_MODELS,
required=False,
help_text=_('Component Type')
)
component_name = forms.CharField(
label=_('Compnent name'),
required=False,
help_text=_('Component Name')
)
@ -1002,52 +1096,62 @@ class InventoryItemRoleImportForm(NetBoxModelImportForm):
class CableImportForm(NetBoxModelImportForm):
# Termination A
side_a_device = CSVModelChoiceField(
label=_('Side a device'),
queryset=Device.objects.all(),
to_field_name='name',
help_text=_('Side A device')
)
side_a_type = CSVContentTypeField(
label=_('Side a type'),
queryset=ContentType.objects.all(),
limit_choices_to=CABLE_TERMINATION_MODELS,
help_text=_('Side A type')
)
side_a_name = forms.CharField(
label=_('Side a name'),
help_text=_('Side A component name')
)
# Termination B
side_b_device = CSVModelChoiceField(
label=_('Side b device'),
queryset=Device.objects.all(),
to_field_name='name',
help_text=_('Side B device')
)
side_b_type = CSVContentTypeField(
label=_('Side b type'),
queryset=ContentType.objects.all(),
limit_choices_to=CABLE_TERMINATION_MODELS,
help_text=_('Side B type')
)
side_b_name = forms.CharField(
label=_('Side b name'),
help_text=_('Side B component name')
)
# Cable attributes
status = CSVChoiceField(
label=_('Status'),
choices=LinkStatusChoices,
required=False,
help_text=_('Connection status')
)
type = CSVChoiceField(
label=_('Type'),
choices=CableTypeChoices,
required=False,
help_text=_('Physical medium classification')
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',
help_text=_('Assigned tenant')
)
length_unit = CSVChoiceField(
label=_('Length unit'),
choices=CableLengthUnitChoices,
required=False,
help_text=_('Length unit')
@ -1110,6 +1214,7 @@ class CableImportForm(NetBoxModelImportForm):
class VirtualChassisImportForm(NetBoxModelImportForm):
master = CSVModelChoiceField(
label=_('Master'),
queryset=Device.objects.all(),
to_field_name='name',
required=False,
@ -1127,11 +1232,13 @@ class VirtualChassisImportForm(NetBoxModelImportForm):
class PowerPanelImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
help_text=_('Name of parent site')
)
location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
to_field_name='name'
@ -1153,40 +1260,54 @@ class PowerPanelImportForm(NetBoxModelImportForm):
class PowerFeedImportForm(NetBoxModelImportForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
to_field_name='name',
help_text=_('Assigned site')
)
power_panel = CSVModelChoiceField(
label=_('Power panel'),
queryset=PowerPanel.objects.all(),
to_field_name='name',
help_text=_('Upstream power panel')
)
location = CSVModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
to_field_name='name',
required=False,
help_text=_("Rack's location (if any)")
)
rack = CSVModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
to_field_name='name',
required=False,
help_text=_('Rack')
)
tenant = CSVModelChoiceField(
queryset=Tenant.objects.all(),
to_field_name='name',
required=False,
help_text=_('Assigned tenant')
)
status = CSVChoiceField(
label=_('Status'),
choices=PowerFeedStatusChoices,
help_text=_('Operational status')
)
type = CSVChoiceField(
label=_('Type'),
choices=PowerFeedTypeChoices,
help_text=_('Primary or redundant')
)
supply = CSVChoiceField(
label=_('Supply'),
choices=PowerFeedSupplyChoices,
help_text=_('Supply type (AC/DC)')
)
phase = CSVChoiceField(
label=_('Phase'),
choices=PowerFeedPhaseChoices,
help_text=_('Single or three-phase')
)
@ -1195,7 +1316,7 @@ class PowerFeedImportForm(NetBoxModelImportForm):
model = PowerFeed
fields = (
'site', 'power_panel', 'location', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase',
'voltage', 'amperage', 'max_utilization', 'description', 'comments', 'tags',
'voltage', 'amperage', 'max_utilization', 'tenant', 'description', 'comments', 'tags',
)
def __init__(self, data=None, *args, **kwargs):
@ -1222,11 +1343,13 @@ class PowerFeedImportForm(NetBoxModelImportForm):
class VirtualDeviceContextImportForm(NetBoxModelImportForm):
device = CSVModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
to_field_name='name',
help_text='Assigned role'
)
tenant = CSVModelChoiceField(
label=_('Tenant'),
queryset=Tenant.objects.all(),
required=False,
to_field_name='name',

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@ -47,7 +47,7 @@ class InterfaceCommonForm(forms.Form):
# Untagged interfaces cannot be assigned tagged VLANs
if self.cleaned_data['mode'] == InterfaceModeChoices.MODE_ACCESS and tagged_vlans:
raise forms.ValidationError({
'mode': "An access interface cannot have tagged VLANs assigned."
'mode': _("An access interface cannot have tagged VLANs assigned.")
})
# Remove all tagged VLAN assignments from "tagged all" interfaces
@ -61,8 +61,10 @@ class InterfaceCommonForm(forms.Form):
if invalid_vlans:
raise forms.ValidationError({
'tagged_vlans': f"The tagged VLANs ({', '.join(invalid_vlans)}) must belong to the same site as "
f"the interface's parent device/VM, or they must be global"
'tagged_vlans': _(
"The tagged VLANs ({vlans}) must belong to the same site as the interface's parent device/VM, "
"or they must be global"
).format(vlans=', '.join(invalid_vlans))
})
@ -105,7 +107,7 @@ class ModuleCommonForm(forms.Form):
# Installing modules with placeholders require that the bay has a position value
if MODULE_TOKEN in template.name and not module_bay.position:
raise forms.ValidationError(
"Cannot install module with placeholder values in a module bay with no position defined"
_("Cannot install module with placeholder values in a module bay with no position defined.")
)
resolved_name = template.name.replace(MODULE_TOKEN, module_bay.position)
@ -114,12 +116,17 @@ class ModuleCommonForm(forms.Form):
# It is not possible to adopt components already belonging to a module
if adopt_components and existing_item and existing_item.module:
raise forms.ValidationError(
f"Cannot adopt {template.component_model.__name__} '{resolved_name}' as it already belongs "
f"to a module"
_("Cannot adopt {name} '{resolved_name}' as it already belongs to a module").format(
name=template.component_model.__name__,
resolved_name=resolved_name
)
)
# If we are not adopting components we error if the component exists
if not adopt_components and resolved_name in installed_components:
raise forms.ValidationError(
f"{template.component_model.__name__} - {resolved_name} already exists"
_("{name} - {resolved_name} already exists").format(
name=template.component_model.__name__,
resolved_name=resolved_name
)
)

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from circuits.models import Circuit, CircuitTermination
from dcim.models import *

View File

@ -1,6 +1,6 @@
from django import forms
from django.contrib.auth import get_user_model
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@ -56,9 +56,11 @@ __all__ = (
class DeviceComponentFilterForm(NetBoxModelFilterSetForm):
name = forms.CharField(
label=_('Name'),
required=False
)
label = forms.CharField(
label=_('Label'),
required=False
)
region_id = DynamicModelMultipleChoiceField(
@ -130,7 +132,7 @@ class RegionFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Region
fieldsets = (
(None, ('q', 'filter_id', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
(_('Contacts'), ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -144,7 +146,7 @@ class SiteGroupFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = SiteGroup
fieldsets = (
(None, ('q', 'filter_id', 'tag', 'parent_id')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
(_('Contacts'), ('contact', 'contact_role', 'contact_group'))
)
parent_id = DynamicModelMultipleChoiceField(
queryset=SiteGroup.objects.all(),
@ -158,11 +160,12 @@ class SiteFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
model = Site
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('status', 'region_id', 'group_id', 'asn_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
(_('Attributes'), ('status', 'region_id', 'group_id', 'asn_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=SiteStatusChoices,
required=False
)
@ -188,9 +191,9 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
model = Location
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
(_('Attributes'), ('region_id', 'site_group_id', 'site_id', 'parent_id', 'status')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -221,6 +224,7 @@ class LocationFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelF
label=_('Parent')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=LocationStatusChoices,
required=False
)
@ -236,12 +240,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
model = Rack
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
('Weight', ('weight', 'max_weight', 'weight_unit')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
(_('Function'), ('status', 'role_id')),
(_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
(_('Weight'), ('weight', 'max_weight', 'weight_unit')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -271,14 +275,17 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
label=_('Location')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=RackStatusChoices,
required=False
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=RackTypeChoices,
required=False
)
width = forms.MultipleChoiceField(
label=_('Width'),
choices=RackWidthChoices,
required=False
)
@ -289,21 +296,26 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
label=_('Role')
)
serial = forms.CharField(
label=_('Serial'),
required=False
)
asset_tag = forms.CharField(
label=_('Asset tag'),
required=False
)
tag = TagFilterField(model)
weight = forms.DecimalField(
label=_('Weight'),
required=False,
min_value=1
)
max_weight = forms.IntegerField(
label=_('Max weight'),
required=False,
min_value=1
)
weight_unit = forms.ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False
)
@ -312,12 +324,12 @@ class RackFilterForm(TenancyFilterForm, ContactModelFilterForm, NetBoxModelFilte
class RackElevationFilterForm(RackFilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
('Function', ('status', 'role_id')),
('Hardware', ('type', 'width', 'serial', 'asset_tag')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
('Weight', ('weight', 'max_weight', 'weight_unit')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'id')),
(_('Function'), ('status', 'role_id')),
(_('Hardware'), ('type', 'width', 'serial', 'asset_tag')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
(_('Weight'), ('weight', 'max_weight', 'weight_unit')),
)
id = DynamicModelMultipleChoiceField(
queryset=Rack.objects.all(),
@ -334,9 +346,9 @@ class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = RackReservation
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('User', ('user_id',)),
('Rack', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
(_('User'), ('user_id',)),
(_('Rack'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -390,7 +402,7 @@ class ManufacturerFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = Manufacturer
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Contacts', ('contact', 'contact_role', 'contact_group'))
(_('Contacts'), ('contact', 'contact_role', 'contact_group'))
)
tag = TagFilterField(model)
@ -399,13 +411,13 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
model = DeviceType
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Hardware', ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')),
('Images', ('has_front_image', 'has_rear_image')),
('Components', (
(_('Hardware'), ('manufacturer_id', 'default_platform_id', 'part_number', 'subdevice_role', 'airflow')),
(_('Images'), ('has_front_image', 'has_rear_image')),
(_('Components'), (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports', 'device_bays', 'module_bays', 'inventory_items',
)),
('Weight', ('weight', 'weight_unit')),
(_('Weight'), ('weight', 'weight_unit')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
@ -418,98 +430,103 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm):
label=_('Default platform')
)
part_number = forms.CharField(
label=_('Part number'),
required=False
)
subdevice_role = forms.MultipleChoiceField(
label=_('Subdevice role'),
choices=add_blank_choice(SubdeviceRoleChoices),
required=False
)
airflow = forms.MultipleChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
has_front_image = forms.NullBooleanField(
required=False,
label='Has a front image',
label=_('Has a front image'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
has_rear_image = forms.NullBooleanField(
required=False,
label='Has a rear image',
label=_('Has a rear image'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
label=_('Has console ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_server_ports = forms.NullBooleanField(
required=False,
label='Has console server ports',
label=_('Has console server ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_ports = forms.NullBooleanField(
required=False,
label='Has power ports',
label=_('Has power ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_outlets = forms.NullBooleanField(
required=False,
label='Has power outlets',
label=_('Has power outlets'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
interfaces = forms.NullBooleanField(
required=False,
label='Has interfaces',
label=_('Has interfaces'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
pass_through_ports = forms.NullBooleanField(
required=False,
label='Has pass-through ports',
label=_('Has pass-through ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
device_bays = forms.NullBooleanField(
required=False,
label='Has device bays',
label=_('Has device bays'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
module_bays = forms.NullBooleanField(
required=False,
label='Has module bays',
label=_('Has module bays'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
inventory_items = forms.NullBooleanField(
required=False,
label='Has inventory items',
label=_('Has inventory items'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model)
weight = forms.DecimalField(
label=_('Weight'),
required=False
)
weight_unit = forms.ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False
)
@ -519,12 +536,12 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
model = ModuleType
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Hardware', ('manufacturer_id', 'part_number')),
('Components', (
(_('Hardware'), ('manufacturer_id', 'part_number')),
(_('Components'), (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces',
'pass_through_ports',
)),
('Weight', ('weight', 'weight_unit')),
(_('Weight'), ('weight', 'weight_unit')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
@ -533,55 +550,58 @@ class ModuleTypeFilterForm(NetBoxModelFilterSetForm):
fetch_trigger='open'
)
part_number = forms.CharField(
label=_('Part number'),
required=False
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
label=_('Has console ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_server_ports = forms.NullBooleanField(
required=False,
label='Has console server ports',
label=_('Has console server ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_ports = forms.NullBooleanField(
required=False,
label='Has power ports',
label=_('Has power ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_outlets = forms.NullBooleanField(
required=False,
label='Has power outlets',
label=_('Has power outlets'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
interfaces = forms.NullBooleanField(
required=False,
label='Has interfaces',
label=_('Has interfaces'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
pass_through_ports = forms.NullBooleanField(
required=False,
label='Has pass-through ports',
label=_('Has pass-through ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
tag = TagFilterField(model)
weight = forms.DecimalField(
label=_('Weight'),
required=False
)
weight_unit = forms.ChoiceField(
label=_('Weight unit'),
choices=add_blank_choice(WeightUnitChoices),
required=False
)
@ -621,15 +641,17 @@ class DeviceFilterForm(
model = Device
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
('Components', (
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Operation'), ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')),
(_('Hardware'), ('manufacturer_id', 'device_type_id', 'platform_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
(_('Components'), (
'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports',
)),
('Miscellaneous', ('has_primary_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data'))
(_('Miscellaneous'), (
'has_primary_ip', 'has_oob_ip', 'virtual_chassis_member', 'config_template_id', 'local_context_data',
))
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -694,22 +716,26 @@ class DeviceFilterForm(
label=_('Platform')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=DeviceStatusChoices,
required=False
)
airflow = forms.MultipleChoiceField(
label=_('Airflow'),
choices=add_blank_choice(DeviceAirflowChoices),
required=False
)
serial = forms.CharField(
label=_('Serial'),
required=False
)
asset_tag = forms.CharField(
label=_('Asset tag'),
required=False
)
mac_address = forms.CharField(
required=False,
label='MAC address'
label=_('MAC address')
)
config_template_id = DynamicModelMultipleChoiceField(
queryset=ConfigTemplate.objects.all(),
@ -718,56 +744,63 @@ class DeviceFilterForm(
)
has_primary_ip = forms.NullBooleanField(
required=False,
label='Has a primary IP',
label=_('Has a primary IP'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
has_oob_ip = forms.NullBooleanField(
required=False,
label='Has an OOB IP',
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
virtual_chassis_member = forms.NullBooleanField(
required=False,
label='Virtual chassis member',
label=_('Virtual chassis member'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_ports = forms.NullBooleanField(
required=False,
label='Has console ports',
label=_('Has console ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
console_server_ports = forms.NullBooleanField(
required=False,
label='Has console server ports',
label=_('Has console server ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_ports = forms.NullBooleanField(
required=False,
label='Has power ports',
label=_('Has power ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
power_outlets = forms.NullBooleanField(
required=False,
label='Has power outlets',
label=_('Has power outlets'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
interfaces = forms.NullBooleanField(
required=False,
label='Has interfaces',
label=_('Has interfaces'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
pass_through_ports = forms.NullBooleanField(
required=False,
label='Has pass-through ports',
label=_('Has pass-through ports'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
@ -782,8 +815,8 @@ class VirtualDeviceContextFilterForm(
model = VirtualDeviceContext
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('device', 'status', 'has_primary_ip')),
('Tenant', ('tenant_group_id', 'tenant_id')),
(_('Attributes'), ('device', 'status', 'has_primary_ip')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
device = DynamicModelMultipleChoiceField(
queryset=Device.objects.all(),
@ -792,12 +825,13 @@ class VirtualDeviceContextFilterForm(
fetch_trigger='open'
)
status = forms.MultipleChoiceField(
label=_('Status'),
required=False,
choices=add_blank_choice(VirtualDeviceContextStatusChoices)
)
has_primary_ip = forms.NullBooleanField(
required=False,
label='Has a primary IP',
label=_('Has a primary IP'),
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
@ -809,7 +843,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
model = Module
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Hardware', ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
(_('Hardware'), ('manufacturer_id', 'module_type_id', 'status', 'serial', 'asset_tag')),
)
manufacturer_id = DynamicModelMultipleChoiceField(
queryset=Manufacturer.objects.all(),
@ -827,13 +861,16 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo
fetch_trigger='open'
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=ModuleStatusChoices,
required=False
)
serial = forms.CharField(
label=_('Serial'),
required=False
)
asset_tag = forms.CharField(
label=_('Asset tag'),
required=False
)
tag = TagFilterField(model)
@ -843,8 +880,8 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = VirtualChassis
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id')),
('Tenant', ('tenant_group_id', 'tenant_id')),
(_('Location'), ('region_id', 'site_group_id', 'site_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -872,9 +909,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = Cable
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('site_id', 'location_id', 'rack_id', 'device_id')),
('Attributes', ('type', 'status', 'color', 'length', 'length_unit')),
('Tenant', ('tenant_group_id', 'tenant_id')),
(_('Location'), ('site_id', 'location_id', 'rack_id', 'device_id')),
(_('Attributes'), ('type', 'status', 'color', 'length', 'length_unit')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -920,20 +957,25 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
label=_('Device')
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=add_blank_choice(CableTypeChoices),
required=False
)
status = forms.MultipleChoiceField(
label=_('Status'),
required=False,
choices=add_blank_choice(LinkStatusChoices)
)
color = ColorField(
label=_('Color'),
required=False
)
length = forms.IntegerField(
label=_('Length'),
required=False
)
length_unit = forms.ChoiceField(
label=_('Length unit'),
choices=add_blank_choice(CableLengthUnitChoices),
required=False
)
@ -944,8 +986,8 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
model = PowerPanel
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Contacts', ('contact', 'contact_role', 'contact_group')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
(_('Contacts'), ('contact', 'contact_role', 'contact_group')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -978,12 +1020,13 @@ class PowerPanelFilterForm(ContactModelFilterForm, NetBoxModelFilterSetForm):
tag = TagFilterField(model)
class PowerFeedFilterForm(NetBoxModelFilterSetForm):
class PowerFeedFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm):
model = PowerFeed
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id')),
(_('Attributes'), ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')),
)
region_id = DynamicModelMultipleChoiceField(
queryset=Region.objects.all(),
@ -1022,28 +1065,35 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
label=_('Rack')
)
status = forms.MultipleChoiceField(
label=_('Status'),
choices=PowerFeedStatusChoices,
required=False
)
type = forms.ChoiceField(
label=_('Type'),
choices=add_blank_choice(PowerFeedTypeChoices),
required=False
)
supply = forms.ChoiceField(
label=_('Supply'),
choices=add_blank_choice(PowerFeedSupplyChoices),
required=False
)
phase = forms.ChoiceField(
label=_('Phase'),
choices=add_blank_choice(PowerFeedPhaseChoices),
required=False
)
voltage = forms.IntegerField(
label=_('Voltage'),
required=False
)
amperage = forms.IntegerField(
label=_('Amperage'),
required=False
)
max_utilization = forms.IntegerField(
label=_('Max utilization'),
required=False
)
tag = TagFilterField(model)
@ -1055,12 +1105,14 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm):
class CabledFilterForm(forms.Form):
cabled = forms.NullBooleanField(
label=_('Cabled'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
occupied = forms.NullBooleanField(
label=_('Occupied'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
@ -1070,6 +1122,7 @@ class CabledFilterForm(forms.Form):
class PathEndpointFilterForm(CabledFilterForm):
connected = forms.NullBooleanField(
label=_('Connected'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
@ -1081,16 +1134,18 @@ class ConsolePortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = ConsolePort
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')),
(_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices,
required=False
)
speed = forms.MultipleChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices,
required=False
)
@ -1101,16 +1156,18 @@ class ConsoleServerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterF
model = ConsoleServerPort
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'speed')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')),
(_('Attributes'), ('name', 'label', 'type', 'speed')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=ConsolePortTypeChoices,
required=False
)
speed = forms.MultipleChoiceField(
label=_('Speed'),
choices=ConsolePortSpeedChoices,
required=False
)
@ -1121,12 +1178,13 @@ class PowerPortFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerPort
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')),
(_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=PowerPortTypeChoices,
required=False
)
@ -1137,12 +1195,13 @@ class PowerOutletFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = PowerOutlet
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Connection', ('cabled', 'connected', 'occupied')),
(_('Attributes'), ('name', 'label', 'type')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=PowerOutletTypeChoices,
required=False
)
@ -1153,13 +1212,13 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
model = Interface
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
('Addressing', ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
('PoE', ('poe_mode', 'poe_type')),
('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
('Connection', ('cabled', 'connected', 'occupied')),
(_('Attributes'), ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')),
(_('Addressing'), ('vrf_id', 'l2vpn_id', 'mac_address', 'wwn')),
(_('PoE'), ('poe_mode', 'poe_type')),
(_('Wireless'), ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id', 'vdc_id')),
(_('Connection'), ('cabled', 'connected', 'occupied')),
)
vdc_id = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
@ -1170,30 +1229,36 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
label=_('Virtual Device Context')
)
kind = forms.MultipleChoiceField(
label=_('Kind'),
choices=InterfaceKindChoices,
required=False
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices,
required=False
)
speed = forms.IntegerField(
label=_('Speed'),
required=False,
widget=NumberWithOptions(
options=InterfaceSpeedChoices
)
)
duplex = forms.MultipleChoiceField(
label=_('Duplex'),
choices=InterfaceDuplexChoices,
required=False
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
mgmt_only = forms.NullBooleanField(
label=_('Mgmt only'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
@ -1201,50 +1266,50 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
)
mac_address = forms.CharField(
required=False,
label='MAC address'
label=_('MAC address')
)
wwn = forms.CharField(
required=False,
label='WWN'
label=_('WWN')
)
poe_mode = forms.MultipleChoiceField(
choices=InterfacePoEModeChoices,
required=False,
label='PoE mode'
label=_('PoE mode')
)
poe_type = forms.MultipleChoiceField(
choices=InterfacePoETypeChoices,
required=False,
label='PoE type'
label=_('PoE type')
)
rf_role = forms.MultipleChoiceField(
choices=WirelessRoleChoices,
required=False,
label='Wireless role'
label=_('Wireless role')
)
rf_channel = forms.MultipleChoiceField(
choices=WirelessChannelChoices,
required=False,
label='Wireless channel'
label=_('Wireless channel')
)
rf_channel_frequency = forms.IntegerField(
required=False,
label='Channel frequency (MHz)'
label=_('Channel frequency (MHz)')
)
rf_channel_width = forms.IntegerField(
required=False,
label='Channel width (MHz)'
label=_('Channel width (MHz)')
)
tx_power = forms.IntegerField(
required=False,
label='Transmit power (dBm)',
label=_('Transmit power (dBm)'),
min_value=0,
max_value=127
)
vrf_id = DynamicModelMultipleChoiceField(
queryset=VRF.objects.all(),
required=False,
label='VRF'
label=_('VRF')
)
l2vpn_id = DynamicModelMultipleChoiceField(
queryset=L2VPN.objects.all(),
@ -1257,17 +1322,19 @@ class InterfaceFilterForm(PathEndpointFilterForm, DeviceComponentFilterForm):
class FrontPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Cable', ('cabled', 'occupied')),
(_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')),
)
model = FrontPort
type = forms.MultipleChoiceField(
label=_('Type'),
choices=PortTypeChoices,
required=False
)
color = ColorField(
label=_('Color'),
required=False
)
tag = TagFilterField(model)
@ -1277,16 +1344,18 @@ class RearPortFilterForm(CabledFilterForm, DeviceComponentFilterForm):
model = RearPort
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'type', 'color')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
('Cable', ('cabled', 'occupied')),
(_('Attributes'), ('name', 'label', 'type', 'color')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Cable'), ('cabled', 'occupied')),
)
type = forms.MultipleChoiceField(
label=_('Type'),
choices=PortTypeChoices,
required=False
)
color = ColorField(
label=_('Color'),
required=False
)
tag = TagFilterField(model)
@ -1296,12 +1365,13 @@ class ModuleBayFilterForm(DeviceComponentFilterForm):
model = ModuleBay
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'position')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Attributes'), ('name', 'label', 'position')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
tag = TagFilterField(model)
position = forms.CharField(
label=_('Position'),
required=False
)
@ -1310,9 +1380,9 @@ class DeviceBayFilterForm(DeviceComponentFilterForm):
model = DeviceBay
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Attributes'), ('name', 'label')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
tag = TagFilterField(model)
@ -1321,9 +1391,9 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
model = InventoryItem
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Attributes', ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
('Device', ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
(_('Attributes'), ('name', 'label', 'role_id', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')),
(_('Device'), ('device_type_id', 'device_role_id', 'device_id', 'virtual_chassis_id')),
)
role_id = DynamicModelMultipleChoiceField(
queryset=InventoryItemRole.objects.all(),
@ -1337,12 +1407,15 @@ class InventoryItemFilterForm(DeviceComponentFilterForm):
label=_('Manufacturer')
)
serial = forms.CharField(
label=_('Serial'),
required=False
)
asset_tag = forms.CharField(
label=_('Asset tag'),
required=False
)
discovered = forms.NullBooleanField(
label=_('Discovered'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES

View File

@ -1,4 +1,5 @@
from django import forms
from django.utils.translation import gettext_lazy as _
__all__ = (
'BaseVCMemberFormSet',
@ -16,6 +17,8 @@ class BaseVCMemberFormSet(forms.BaseModelFormSet):
vc_position = form.cleaned_data.get('vc_position')
if vc_position:
if vc_position in vc_position_list:
error_msg = f"A virtual chassis member already exists in position {vc_position}."
error_msg = _("A virtual chassis member already exists in position {vc_position}.").format(
vc_position=vc_position
)
form.add_error('vc_position', error_msg)
vc_position_list.append(vc_position)

View File

@ -1,7 +1,7 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneFormField
from dcim.choices import *
@ -70,13 +70,14 @@ __all__ = (
class RegionForm(NetBoxModelForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=Region.objects.all(),
required=False
)
slug = SlugField()
fieldsets = (
('Region', (
(_('Region'), (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
@ -90,13 +91,14 @@ class RegionForm(NetBoxModelForm):
class SiteGroupForm(NetBoxModelForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=SiteGroup.objects.all(),
required=False
)
slug = SlugField()
fieldsets = (
('Site Group', (
(_('Site Group'), (
'parent', 'name', 'slug', 'description', 'tags',
)),
)
@ -110,10 +112,12 @@ class SiteGroupForm(NetBoxModelForm):
class SiteForm(TenancyForm, NetBoxModelForm):
region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(),
required=False
)
group = DynamicModelChoiceField(
label=_('Group'),
queryset=SiteGroup.objects.all(),
required=False
)
@ -124,17 +128,18 @@ class SiteForm(TenancyForm, NetBoxModelForm):
)
slug = SlugField()
time_zone = TimeZoneFormField(
label=_('Time zone'),
choices=add_blank_choice(TimeZoneFormField().choices),
required=False
)
comments = CommentField()
fieldsets = (
('Site', (
(_('Site'), (
'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags',
)),
('Tenancy', ('tenant_group', 'tenant')),
('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')),
(_('Tenancy'), ('tenant_group', 'tenant')),
(_('Contact Info'), ('physical_address', 'shipping_address', 'latitude', 'longitude')),
)
class Meta:
@ -159,10 +164,12 @@ class SiteForm(TenancyForm, NetBoxModelForm):
class LocationForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=Location.objects.all(),
required=False,
query_params={
@ -172,8 +179,8 @@ class LocationForm(TenancyForm, NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Location', ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
(_('Location'), ('site', 'parent', 'name', 'slug', 'status', 'description', 'tags')),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
class Meta:
@ -187,7 +194,7 @@ class RackRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Rack Role', (
(_('Rack Role'), (
'name', 'slug', 'color', 'description', 'tags',
)),
)
@ -201,10 +208,12 @@ class RackRoleForm(NetBoxModelForm):
class RackForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
@ -212,6 +221,7 @@ class RackForm(TenancyForm, NetBoxModelForm):
}
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=RackRole.objects.all(),
required=False
)
@ -228,14 +238,17 @@ class RackForm(TenancyForm, NetBoxModelForm):
class RackReservationForm(TenancyForm, NetBoxModelForm):
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
selector=True
)
units = NumericArrayField(
label=_('Units'),
base_field=forms.IntegerField(),
help_text=_("Comma-separated list of numeric unit IDs. A range may be specified using a hyphen.")
)
user = forms.ModelChoiceField(
label=_('User'),
queryset=get_user_model().objects.order_by(
'username'
)
@ -243,8 +256,8 @@ class RackReservationForm(TenancyForm, NetBoxModelForm):
comments = CommentField()
fieldsets = (
('Reservation', ('rack', 'units', 'user', 'description', 'tags')),
('Tenancy', ('tenant_group', 'tenant')),
(_('Reservation'), ('rack', 'units', 'user', 'description', 'tags')),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
class Meta:
@ -258,7 +271,7 @@ class ManufacturerForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Manufacturer', (
(_('Manufacturer'), (
'name', 'slug', 'description', 'tags',
)),
)
@ -272,23 +285,26 @@ class ManufacturerForm(NetBoxModelForm):
class DeviceTypeForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all()
)
default_platform = DynamicModelChoiceField(
label=_('Default platform'),
queryset=Platform.objects.all(),
required=False
)
slug = SlugField(
label=_('Slug'),
slug_source='model'
)
comments = CommentField()
fieldsets = (
('Device Type', ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
('Chassis', (
(_('Device Type'), ('manufacturer', 'model', 'slug', 'default_platform', 'description', 'tags')),
(_('Chassis'), (
'u_height', 'is_full_depth', 'part_number', 'subdevice_role', 'airflow', 'weight', 'weight_unit',
)),
('Images', ('front_image', 'rear_image')),
(_('Images'), ('front_image', 'rear_image')),
)
class Meta:
@ -310,13 +326,14 @@ class DeviceTypeForm(NetBoxModelForm):
class ModuleTypeForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all()
)
comments = CommentField()
fieldsets = (
('Module Type', ('manufacturer', 'model', 'part_number', 'description', 'tags')),
('Weight', ('weight', 'weight_unit'))
(_('Module Type'), ('manufacturer', 'model', 'part_number', 'description', 'tags')),
(_('Weight'), ('weight', 'weight_unit'))
)
class Meta:
@ -328,13 +345,14 @@ class ModuleTypeForm(NetBoxModelForm):
class DeviceRoleForm(NetBoxModelForm):
config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
required=False
)
slug = SlugField()
fieldsets = (
('Device Role', (
(_('Device Role'), (
'name', 'slug', 'color', 'vm_role', 'config_template', 'description', 'tags',
)),
)
@ -348,19 +366,22 @@ class DeviceRoleForm(NetBoxModelForm):
class PlatformForm(NetBoxModelForm):
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
required=False
)
config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
required=False
)
slug = SlugField(
label=_('Slug'),
max_length=64
)
fieldsets = (
('Platform', ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
(_('Platform'), ('name', 'slug', 'manufacturer', 'config_template', 'description', 'tags')),
)
class Meta:
@ -372,10 +393,12 @@ class PlatformForm(NetBoxModelForm):
class DeviceForm(TenancyForm, NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
@ -386,6 +409,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
}
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
query_params={
@ -394,6 +418,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
}
)
position = forms.DecimalField(
label=_('Position'),
required=False,
help_text=_("The lowest-numbered unit occupied by the device"),
widget=APISelect(
@ -405,17 +430,21 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
)
)
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all(),
selector=True
)
device_role = DynamicModelChoiceField(
label=_('Device role'),
queryset=DeviceRole.objects.all()
)
platform = DynamicModelChoiceField(
label=_('Platform'),
queryset=Platform.objects.all(),
required=False
)
cluster = DynamicModelChoiceField(
label=_('Cluster'),
queryset=Cluster.objects.all(),
required=False,
selector=True
@ -426,6 +455,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
label=''
)
virtual_chassis = DynamicModelChoiceField(
label=_('Virtual chassis'),
queryset=VirtualChassis.objects.all(),
required=False,
selector=True
@ -441,6 +471,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
help_text=_("The priority of the device in the virtual chassis")
)
config_template = DynamicModelChoiceField(
label=_('Config template'),
queryset=ConfigTemplate.objects.all(),
required=False
)
@ -449,9 +480,9 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
model = Device
fields = [
'name', 'device_role', 'device_type', 'serial', 'asset_tag', 'site', 'rack', 'location', 'position', 'face',
'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'cluster',
'latitude', 'longitude', 'status', 'airflow', 'platform', 'primary_ip4', 'primary_ip6', 'oob_ip', 'cluster',
'tenant_group', 'tenant', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
'comments', 'tags', 'local_context_data'
'comments', 'tags', 'local_context_data',
]
def __init__(self, *args, **kwargs):
@ -460,6 +491,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
if self.instance.pk:
# Compile list of choices for primary IPv4 and IPv6 addresses
oob_ip_choices = [(None, '---------')]
for family in [4, 6]:
ip_choices = [(None, '---------')]
@ -475,6 +507,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
if interface_ips:
ip_list = [(ip.id, f'{ip.address} ({ip.assigned_object})') for ip in interface_ips]
ip_choices.append(('Interface IPs', ip_list))
oob_ip_choices.extend(ip_list)
# Collect NAT IPs
nat_ips = IPAddress.objects.prefetch_related('nat_inside').filter(
address__family=family,
@ -485,6 +518,7 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
ip_list = [(ip.id, f'{ip.address} (NAT)') for ip in nat_ips]
ip_choices.append(('NAT IPs', ip_list))
self.fields['primary_ip{}'.format(family)].choices = ip_choices
self.fields['oob_ip'].choices = oob_ip_choices
# If editing an existing device, exclude it from the list of occupied rack units. This ensures that a device
# can be flipped from one face to another.
@ -504,6 +538,8 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
self.fields['primary_ip4'].widget.attrs['readonly'] = True
self.fields['primary_ip6'].choices = []
self.fields['primary_ip6'].widget.attrs['readonly'] = True
self.fields['oob_ip'].choices = []
self.fields['oob_ip'].widget.attrs['readonly'] = True
# Rack position
position = self.data.get('position') or self.initial.get('position')
@ -513,36 +549,41 @@ class DeviceForm(TenancyForm, NetBoxModelForm):
class ModuleForm(ModuleCommonForm, NetBoxModelForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
initial_params={
'modulebays': '$module_bay'
}
)
module_bay = DynamicModelChoiceField(
label=_('Module bay'),
queryset=ModuleBay.objects.all(),
query_params={
'device_id': '$device'
}
)
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
selector=True
)
comments = CommentField()
replicate_components = forms.BooleanField(
label=_('Replicate components'),
required=False,
initial=True,
help_text=_("Automatically populate components associated with this module type")
)
adopt_components = forms.BooleanField(
label=_('Adopt components'),
required=False,
initial=False,
help_text=_("Adopt already existing components")
)
fieldsets = (
('Module', ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
('Hardware', (
(_('Module'), ('device', 'module_bay', 'module_type', 'status', 'description', 'tags')),
(_('Hardware'), (
'serial', 'asset_tag', 'replicate_components', 'adopt_components',
)),
)
@ -576,17 +617,19 @@ class CableForm(TenancyForm, NetBoxModelForm):
]
error_messages = {
'length': {
'max_value': 'Maximum length is 32767 (any unit)'
'max_value': _('Maximum length is 32767 (any unit)')
}
}
class PowerPanelForm(NetBoxModelForm):
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
selector=True
)
location = DynamicModelChoiceField(
label=_('Location'),
queryset=Location.objects.all(),
required=False,
query_params={
@ -606,12 +649,14 @@ class PowerPanelForm(NetBoxModelForm):
]
class PowerFeedForm(NetBoxModelForm):
class PowerFeedForm(TenancyForm, NetBoxModelForm):
power_panel = DynamicModelChoiceField(
label=_('Power panel'),
queryset=PowerPanel.objects.all(),
selector=True
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
selector=True
@ -619,15 +664,16 @@ class PowerFeedForm(NetBoxModelForm):
comments = CommentField()
fieldsets = (
('Power Feed', ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
(_('Power Feed'), ('power_panel', 'rack', 'name', 'status', 'type', 'description', 'mark_connected', 'tags')),
(_('Characteristics'), ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')),
(_('Tenancy'), ('tenant_group', 'tenant')),
)
class Meta:
model = PowerFeed
fields = [
'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization', 'description', 'comments', 'tags',
'max_utilization', 'tenant_group', 'tenant', 'description', 'comments', 'tags'
]
@ -637,6 +683,7 @@ class PowerFeedForm(NetBoxModelForm):
class VirtualChassisForm(NetBoxModelForm):
master = forms.ModelChoiceField(
label=_('Master'),
queryset=Device.objects.all(),
required=False,
)
@ -700,6 +747,7 @@ class DeviceVCMembershipForm(forms.ModelForm):
class VCMemberSelectForm(BootstrapMixin, forms.Form):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
query_params={
'virtual_chassis_id': 'null',
@ -722,6 +770,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all()
)
@ -735,10 +784,12 @@ class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
class ModularComponentTemplateForm(ComponentTemplateForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all().all(),
required=False
)
module_type = DynamicModelChoiceField(
label=_('Module type'),
queryset=ModuleType.objects.all(),
required=False
)
@ -791,6 +842,7 @@ class PowerPortTemplateForm(ModularComponentTemplateForm):
class PowerOutletTemplateForm(ModularComponentTemplateForm):
power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPortTemplate.objects.all(),
required=False,
query_params={
@ -811,6 +863,7 @@ class PowerOutletTemplateForm(ModularComponentTemplateForm):
class InterfaceTemplateForm(ModularComponentTemplateForm):
bridge = DynamicModelChoiceField(
label=_('Bridge'),
queryset=InterfaceTemplate.objects.all(),
required=False,
query_params={
@ -821,18 +874,20 @@ class InterfaceTemplateForm(ModularComponentTemplateForm):
fieldsets = (
(None, ('device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'bridge')),
('PoE', ('poe_mode', 'poe_type'))
(_('PoE'), ('poe_mode', 'poe_type')),
(_('Wireless'), ('rf_role',)),
)
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge',
'device_type', 'module_type', 'name', 'label', 'type', 'mgmt_only', 'enabled', 'description', 'poe_mode', 'poe_type', 'bridge', 'rf_role',
]
class FrontPortTemplateForm(ModularComponentTemplateForm):
rear_port = DynamicModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
required=False,
query_params={
@ -894,6 +949,7 @@ class DeviceBayTemplateForm(ComponentTemplateForm):
class InventoryItemTemplateForm(ComponentTemplateForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=InventoryItemTemplate.objects.all(),
required=False,
query_params={
@ -901,10 +957,12 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
}
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
required=False
)
@ -940,6 +998,7 @@ class InventoryItemTemplateForm(ComponentTemplateForm):
class DeviceComponentForm(NetBoxModelForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
selector=True
)
@ -954,6 +1013,7 @@ class DeviceComponentForm(NetBoxModelForm):
class ModularDeviceComponentForm(DeviceComponentForm):
module = DynamicModelChoiceField(
label=_('Module'),
queryset=Module.objects.all(),
required=False,
query_params={
@ -1010,6 +1070,7 @@ class PowerPortForm(ModularDeviceComponentForm):
class PowerOutletForm(ModularDeviceComponentForm):
power_port = DynamicModelChoiceField(
label=_('Power port'),
queryset=PowerPort.objects.all(),
required=False,
query_params={
@ -1036,7 +1097,7 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
vdcs = DynamicModelMultipleChoiceField(
queryset=VirtualDeviceContext.objects.all(),
required=False,
label='Virtual Device Contexts',
label=_('Virtual device contexts'),
query_params={
'device_id': '$device',
}
@ -1114,13 +1175,13 @@ class InterfaceForm(InterfaceCommonForm, ModularDeviceComponentForm):
)
fieldsets = (
('Interface', ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
('Addressing', ('vrf', 'mac_address', 'wwn')),
('Operation', ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
('Related Interfaces', ('parent', 'bridge', 'lag')),
('PoE', ('poe_mode', 'poe_type')),
('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
('Wireless', (
(_('Interface'), ('device', 'module', 'name', 'label', 'type', 'speed', 'duplex', 'description', 'tags')),
(_('Addressing'), ('vrf', 'mac_address', 'wwn')),
(_('Operation'), ('vdcs', 'mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')),
(_('Related Interfaces'), ('parent', 'bridge', 'lag')),
(_('PoE'), ('poe_mode', 'poe_type')),
(_('802.1Q Switching'), ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')),
(_('Wireless'), (
'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans',
)),
)
@ -1226,6 +1287,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
class InventoryItemForm(DeviceComponentForm):
parent = DynamicModelChoiceField(
label=_('Parent'),
queryset=InventoryItem.objects.all(),
required=False,
query_params={
@ -1233,10 +1295,12 @@ class InventoryItemForm(DeviceComponentForm):
}
)
role = DynamicModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(),
required=False
)
manufacturer = DynamicModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
required=False
)
@ -1300,8 +1364,8 @@ class InventoryItemForm(DeviceComponentForm):
)
fieldsets = (
('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')),
(_('Inventory Item'), ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')),
(_('Hardware'), ('manufacturer', 'part_id', 'serial', 'asset_tag')),
)
class Meta:
@ -1352,7 +1416,7 @@ class InventoryItemForm(DeviceComponentForm):
) if self.cleaned_data[field]
]
if len(selected_objects) > 1:
raise forms.ValidationError("An InventoryItem can only be assigned to a single component.")
raise forms.ValidationError(_("An InventoryItem can only be assigned to a single component."))
elif selected_objects:
self.instance.component = self.cleaned_data[selected_objects[0]]
else:
@ -1366,7 +1430,7 @@ class InventoryItemRoleForm(NetBoxModelForm):
slug = SlugField()
fieldsets = (
('Inventory Item Role', (
(_('Inventory Item Role'), (
'name', 'slug', 'color', 'description', 'tags',
)),
)
@ -1380,12 +1444,13 @@ class InventoryItemRoleForm(NetBoxModelForm):
class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
selector=True
)
primary_ip4 = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
label='Primary IPv4',
label=_('Primary IPv4'),
required=False,
query_params={
'device_id': '$device',
@ -1394,7 +1459,7 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
)
primary_ip6 = DynamicModelChoiceField(
queryset=IPAddress.objects.all(),
label='Primary IPv6',
label=_('Primary IPv6'),
required=False,
query_params={
'device_id': '$device',
@ -1403,8 +1468,8 @@ class VirtualDeviceContextForm(TenancyForm, NetBoxModelForm):
)
fieldsets = (
('Virtual Device Context', ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
('Tenancy', ('tenant_group', 'tenant'))
(_('Virtual Device Context'), ('device', 'name', 'status', 'identifier', 'primary_ip4', 'primary_ip6', 'tags')),
(_('Tenancy'), ('tenant_group', 'tenant'))
)
class Meta:

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.models import *
from netbox.forms import NetBoxModelForm
@ -38,8 +38,11 @@ class ComponentCreateForm(forms.Form):
Subclass this form when facilitating the creation of one or more component or component template objects based on
a name pattern.
"""
name = ExpandableNameField()
name = ExpandableNameField(
label=_('Name'),
)
label = ExpandableNameField(
label=_('Label'),
required=False,
help_text=_('Alphanumeric ranges are supported. (Must match the number of objects being created.)')
)
@ -57,8 +60,9 @@ class ComponentCreateForm(forms.Form):
value_count = len(self.cleaned_data[field_name])
if self.cleaned_data[field_name] and value_count != pattern_count:
raise forms.ValidationError({
field_name: f'The provided pattern specifies {value_count} values, but {pattern_count} are '
f'expected.'
field_name: _(
"The provided pattern specifies {value_count} values, but {pattern_count} are expected."
).format(value_count=value_count, pattern_count=pattern_count)
}, code='label_pattern_mismatch')
@ -222,12 +226,14 @@ class InterfaceCreateForm(ComponentCreateForm, model_forms.InterfaceForm):
super().__init__(*args, **kwargs)
if 'module' in self.fields:
self.fields['name'].help_text += ' The string <code>{module}</code> will be replaced with the position ' \
'of the assigned module, if any'
self.fields['name'].help_text += _(
"The string <code>{module}</code> will be replaced with the position of the assigned module, if any."
)
class FrontPortCreateForm(ComponentCreateForm, model_forms.FrontPortForm):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
selector=True,
widget=APISelect(
@ -329,6 +335,7 @@ class InventoryItemCreateForm(ComponentCreateForm, model_forms.InventoryItemForm
class VirtualChassisCreateForm(NetBoxModelForm):
region = DynamicModelChoiceField(
label=_('Region'),
queryset=Region.objects.all(),
required=False,
initial_params={
@ -336,6 +343,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
}
)
site_group = DynamicModelChoiceField(
label=_('Site group'),
queryset=SiteGroup.objects.all(),
required=False,
initial_params={
@ -343,6 +351,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
}
)
site = DynamicModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
required=False,
query_params={
@ -351,6 +360,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
}
)
rack = DynamicModelChoiceField(
label=_('Rack'),
queryset=Rack.objects.all(),
required=False,
null_option='None',
@ -359,6 +369,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
}
)
members = DynamicModelMultipleChoiceField(
label=_('Members'),
queryset=Device.objects.all(),
required=False,
query_params={
@ -367,6 +378,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
}
)
initial_position = forms.IntegerField(
label=_('Initial position'),
initial=1,
required=False,
help_text=_('Position of the first member device. Increases by one for each additional member.')
@ -383,7 +395,7 @@ class VirtualChassisCreateForm(NetBoxModelForm):
if self.cleaned_data['members'] and self.cleaned_data['initial_position'] is None:
raise forms.ValidationError({
'initial_position': "A position must be specified for the first VC member."
'initial_position': _("A position must be specified for the first VC member.")
})
def save(self, *args, **kwargs):

View File

@ -1,9 +1,10 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
from dcim.models import *
from utilities.forms import BootstrapMixin
from wireless.choices import WirelessRoleChoices
__all__ = (
'ConsolePortTemplateImportForm',
@ -56,6 +57,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
power_port = forms.ModelChoiceField(
label=_('Power port'),
queryset=PowerPortTemplate.objects.all(),
to_field_name='name',
required=False
@ -84,6 +86,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
class InterfaceTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices.CHOICES
)
poe_mode = forms.ChoiceField(
@ -96,19 +99,27 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
required=False,
label=_('PoE type')
)
rf_role = forms.ChoiceField(
choices=WirelessRoleChoices,
required=False,
label=_('Wireless role')
)
class Meta:
model = InterfaceTemplate
fields = [
'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'poe_mode', 'poe_type',
'device_type', 'module_type', 'name', 'label', 'type', 'enabled', 'mgmt_only', 'description', 'poe_mode',
'poe_type', 'rf_role'
]
class FrontPortTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField(
label=_('Type'),
choices=PortTypeChoices.CHOICES
)
rear_port = forms.ModelChoiceField(
label=_('Rear port'),
queryset=RearPortTemplate.objects.all(),
to_field_name='name'
)
@ -136,6 +147,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
class RearPortTemplateImportForm(ComponentTemplateImportForm):
type = forms.ChoiceField(
label=_('Type'),
choices=PortTypeChoices.CHOICES
)
@ -166,15 +178,18 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
class InventoryItemTemplateImportForm(ComponentTemplateImportForm):
parent = forms.ModelChoiceField(
label=_('Parent'),
queryset=InventoryItemTemplate.objects.all(),
required=False
)
role = forms.ModelChoiceField(
label=_('Role'),
queryset=InventoryItemRole.objects.all(),
to_field_name='name',
required=False
)
manufacturer = forms.ModelChoiceField(
label=_('Manufacturer'),
queryset=Manufacturer.objects.all(),
to_field_name='name',
required=False

View File

@ -53,23 +53,23 @@ class LinkPeerType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == CircuitTermination:
if type(instance) is CircuitTermination:
return CircuitTerminationType
if type(instance) == ConsolePortType:
if type(instance) is ConsolePortType:
return ConsolePortType
if type(instance) == ConsoleServerPort:
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) == FrontPort:
if type(instance) is FrontPort:
return FrontPortType
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == PowerFeed:
if type(instance) is PowerFeed:
return PowerFeedType
if type(instance) == PowerOutlet:
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) == PowerPort:
if type(instance) is PowerPort:
return PowerPortType
if type(instance) == RearPort:
if type(instance) is RearPort:
return RearPortType
@ -89,23 +89,23 @@ class CableTerminationTerminationType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == CircuitTermination:
if type(instance) is CircuitTermination:
return CircuitTerminationType
if type(instance) == ConsolePortType:
if type(instance) is ConsolePortType:
return ConsolePortType
if type(instance) == ConsoleServerPort:
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) == FrontPort:
if type(instance) is FrontPort:
return FrontPortType
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == PowerFeed:
if type(instance) is PowerFeed:
return PowerFeedType
if type(instance) == PowerOutlet:
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) == PowerPort:
if type(instance) is PowerPort:
return PowerPortType
if type(instance) == RearPort:
if type(instance) is RearPort:
return RearPortType
@ -123,19 +123,19 @@ class InventoryItemTemplateComponentType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == ConsolePortTemplate:
if type(instance) is ConsolePortTemplate:
return ConsolePortTemplateType
if type(instance) == ConsoleServerPortTemplate:
if type(instance) is ConsoleServerPortTemplate:
return ConsoleServerPortTemplateType
if type(instance) == FrontPortTemplate:
if type(instance) is FrontPortTemplate:
return FrontPortTemplateType
if type(instance) == InterfaceTemplate:
if type(instance) is InterfaceTemplate:
return InterfaceTemplateType
if type(instance) == PowerOutletTemplate:
if type(instance) is PowerOutletTemplate:
return PowerOutletTemplateType
if type(instance) == PowerPortTemplate:
if type(instance) is PowerPortTemplate:
return PowerPortTemplateType
if type(instance) == RearPortTemplate:
if type(instance) is RearPortTemplate:
return RearPortTemplateType
@ -153,17 +153,17 @@ class InventoryItemComponentType(graphene.Union):
@classmethod
def resolve_type(cls, instance, info):
if type(instance) == ConsolePort:
if type(instance) is ConsolePort:
return ConsolePortType
if type(instance) == ConsoleServerPort:
if type(instance) is ConsoleServerPort:
return ConsoleServerPortType
if type(instance) == FrontPort:
if type(instance) is FrontPort:
return FrontPortType
if type(instance) == Interface:
if type(instance) is Interface:
return InterfaceType
if type(instance) == PowerOutlet:
if type(instance) is PowerOutlet:
return PowerOutletType
if type(instance) == PowerPort:
if type(instance) is PowerPort:
return PowerPortType
if type(instance) == RearPort:
if type(instance) is RearPort:
return RearPortType

View File

@ -277,6 +277,9 @@ class InterfaceTemplateType(ComponentTemplateObjectType):
def resolve_poe_type(self, info):
return self.poe_type or None
def resolve_rf_role(self, info):
return self.rf_role or None
class InventoryItemType(ComponentObjectType):
component = graphene.Field('dcim.graphql.gfk_mixins.InventoryItemComponentType')

View File

@ -0,0 +1,25 @@
# Generated by Django 4.1.9 on 2023-07-24 20:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('ipam', '0066_iprange_mark_utilized'),
('dcim', '0174_rack_starting_unit'),
]
operations = [
migrations.AddField(
model_name='device',
name='oob_ip',
field=models.OneToOneField(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='ipam.ipaddress',
),
),
]

View File

@ -0,0 +1,108 @@
from django.db import migrations
from django.db.models import Count
import utilities.fields
def recalculate_device_counts(apps, schema_editor):
Device = apps.get_model("dcim", "Device")
devices = list(Device.objects.all().annotate(
_console_port_count=Count('consoleports', distinct=True),
_console_server_port_count=Count('consoleserverports', distinct=True),
_power_port_count=Count('powerports', distinct=True),
_power_outlet_count=Count('poweroutlets', distinct=True),
_interface_count=Count('interfaces', distinct=True),
_front_port_count=Count('frontports', distinct=True),
_rear_port_count=Count('rearports', distinct=True),
_device_bay_count=Count('devicebays', distinct=True),
_module_bay_count=Count('modulebays', distinct=True),
_inventory_item_count=Count('inventoryitems', distinct=True),
))
for device in devices:
device.console_port_count = device._console_port_count
device.console_server_port_count = device._console_server_port_count
device.power_port_count = device._power_port_count
device.power_outlet_count = device._power_outlet_count
device.interface_count = device._interface_count
device.front_port_count = device._front_port_count
device.rear_port_count = device._rear_port_count
device.device_bay_count = device._device_bay_count
device.module_bay_count = device._module_bay_count
device.inventory_item_count = device._inventory_item_count
Device.objects.bulk_update(devices, [
'console_port_count',
'console_server_port_count',
'power_port_count',
'power_outlet_count',
'interface_count',
'front_port_count',
'rear_port_count',
'device_bay_count',
'module_bay_count',
'inventory_item_count',
])
class Migration(migrations.Migration):
dependencies = [
('dcim', '0175_device_oob_ip'),
]
operations = [
migrations.AddField(
model_name='device',
name='console_port_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ConsolePort'),
),
migrations.AddField(
model_name='device',
name='console_server_port_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ConsoleServerPort'),
),
migrations.AddField(
model_name='device',
name='power_port_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.PowerPort'),
),
migrations.AddField(
model_name='device',
name='power_outlet_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.PowerOutlet'),
),
migrations.AddField(
model_name='device',
name='interface_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.Interface'),
),
migrations.AddField(
model_name='device',
name='front_port_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.FrontPort'),
),
migrations.AddField(
model_name='device',
name='rear_port_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.RearPort'),
),
migrations.AddField(
model_name='device',
name='device_bay_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.DeviceBay'),
),
migrations.AddField(
model_name='device',
name='module_bay_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.ModuleBay'),
),
migrations.AddField(
model_name='device',
name='inventory_item_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device', to_model='dcim.InventoryItem'),
),
migrations.RunPython(
recalculate_device_counts,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,108 @@
from django.db import migrations
from django.db.models import Count
import utilities.fields
def recalculate_devicetype_template_counts(apps, schema_editor):
DeviceType = apps.get_model("dcim", "DeviceType")
device_types = list(DeviceType.objects.all().annotate(
_console_port_template_count=Count('consoleporttemplates', distinct=True),
_console_server_port_template_count=Count('consoleserverporttemplates', distinct=True),
_power_port_template_count=Count('powerporttemplates', distinct=True),
_power_outlet_template_count=Count('poweroutlettemplates', distinct=True),
_interface_template_count=Count('interfacetemplates', distinct=True),
_front_port_template_count=Count('frontporttemplates', distinct=True),
_rear_port_template_count=Count('rearporttemplates', distinct=True),
_device_bay_template_count=Count('devicebaytemplates', distinct=True),
_module_bay_template_count=Count('modulebaytemplates', distinct=True),
_inventory_item_template_count=Count('inventoryitemtemplates', distinct=True),
))
for devicetype in device_types:
devicetype.console_port_template_count = devicetype._console_port_template_count
devicetype.console_server_port_template_count = devicetype._console_server_port_template_count
devicetype.power_port_template_count = devicetype._power_port_template_count
devicetype.power_outlet_template_count = devicetype._power_outlet_template_count
devicetype.interface_template_count = devicetype._interface_template_count
devicetype.front_port_template_count = devicetype._front_port_template_count
devicetype.rear_port_template_count = devicetype._rear_port_template_count
devicetype.device_bay_template_count = devicetype._device_bay_template_count
devicetype.module_bay_template_count = devicetype._module_bay_template_count
devicetype.inventory_item_template_count = devicetype._inventory_item_template_count
DeviceType.objects.bulk_update(device_types, [
'console_port_template_count',
'console_server_port_template_count',
'power_port_template_count',
'power_outlet_template_count',
'interface_template_count',
'front_port_template_count',
'rear_port_template_count',
'device_bay_template_count',
'module_bay_template_count',
'inventory_item_template_count',
])
class Migration(migrations.Migration):
dependencies = [
('dcim', '0176_device_component_counters'),
]
operations = [
migrations.AddField(
model_name='devicetype',
name='console_port_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ConsolePortTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='console_server_port_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ConsoleServerPortTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='power_port_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.PowerPortTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='power_outlet_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.PowerOutletTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='interface_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.InterfaceTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='front_port_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.FrontPortTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='rear_port_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.RearPortTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='device_bay_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.DeviceBayTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='module_bay_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.ModuleBayTemplate'),
),
migrations.AddField(
model_name='devicetype',
name='inventory_item_template_count',
field=utilities.fields.CounterCacheField(default=0, to_field='device_type', to_model='dcim.InventoryItemTemplate'),
),
migrations.RunPython(
recalculate_devicetype_template_counts,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,35 @@
from django.db import migrations
from django.db.models import Count
import utilities.fields
def populate_virtualchassis_members(apps, schema_editor):
VirtualChassis = apps.get_model('dcim', 'VirtualChassis')
vcs = list(VirtualChassis.objects.annotate(_member_count=Count('members', distinct=True)))
for vc in vcs:
vc.member_count = vc._member_count
VirtualChassis.objects.bulk_update(vcs, ['member_count'])
class Migration(migrations.Migration):
dependencies = [
('dcim', '0177_devicetype_component_counters'),
]
operations = [
migrations.AddField(
model_name='virtualchassis',
name='member_count',
field=utilities.fields.CounterCacheField(
default=0, to_field='virtual_chassis', to_model='dcim.Device'
),
),
migrations.RunPython(
code=populate_virtualchassis_members,
reverse_code=migrations.RunPython.noop
),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2023-07-18 07:55
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('dcim', '0178_virtual_chassis_member_counter'),
]
operations = [
migrations.AddField(
model_name='interfacetemplate',
name='rf_role',
field=models.CharField(blank=True, max_length=30),
),
]

View File

@ -0,0 +1,20 @@
# Generated by Django 4.1.8 on 2023-07-29 11:29
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tenancy', '0010_tenant_relax_uniqueness'),
('dcim', '0179_interfacetemplate_rf_role'),
]
operations = [
migrations.AddField(
model_name='powerfeed',
name='tenant',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='power_feeds', to='tenancy.tenant'),
),
]

View File

@ -8,6 +8,7 @@ from django.db import models
from django.db.models import Sum
from django.dispatch import Signal
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@ -40,11 +41,13 @@ class Cable(PrimaryModel):
A physical connection between two endpoints.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=CableTypeChoices,
blank=True
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=LinkStatusChoices,
default=LinkStatusChoices.STATUS_CONNECTED
@ -57,19 +60,23 @@ class Cable(PrimaryModel):
null=True
)
label = models.CharField(
verbose_name=_('label'),
max_length=100,
blank=True
)
color = ColorField(
verbose_name=_('color'),
blank=True
)
length = models.DecimalField(
verbose_name=_('length'),
max_digits=8,
decimal_places=2,
blank=True,
null=True
)
length_unit = models.CharField(
verbose_name=_('length unit'),
max_length=50,
choices=CableLengthUnitChoices,
blank=True,
@ -235,7 +242,7 @@ class CableTermination(ChangeLoggedModel):
cable_end = models.CharField(
max_length=1,
choices=CableEndChoices,
verbose_name='End'
verbose_name=_('end')
)
termination_type = models.ForeignKey(
to=ContentType,
@ -403,15 +410,19 @@ class CablePath(models.Model):
`_nodes` retains a flattened list of all nodes within the path to enable simple filtering.
"""
path = models.JSONField(
verbose_name=_('path'),
default=list
)
is_active = models.BooleanField(
verbose_name=_('is active'),
default=False
)
is_complete = models.BooleanField(
verbose_name=_('is complete'),
default=False
)
is_split = models.BooleanField(
verbose_name=_('is split'),
default=False
)
_nodes = PathField()

View File

@ -3,7 +3,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import *
@ -12,6 +12,8 @@ from netbox.models import ChangeLoggedModel
from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface
from utilities.tracking import TrackingModelMixin
from wireless.choices import WirelessRoleChoices
from .device_components import (
ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
RearPort,
@ -32,17 +34,18 @@ __all__ = (
)
class ComponentTemplateModel(ChangeLoggedModel):
class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
device_type = models.ForeignKey(
to='dcim.DeviceType',
on_delete=models.CASCADE,
related_name='%(class)ss'
)
name = models.CharField(
verbose_name=_('name'),
max_length=64,
help_text="""
{module} is accepted as a substitution for the module bay position when attached to a module type.
"""
help_text=_(
"{module} is accepted as a substitution for the module bay position when attached to a module type."
)
)
_name = NaturalOrderingField(
target_field='name',
@ -50,11 +53,13 @@ class ComponentTemplateModel(ChangeLoggedModel):
blank=True
)
label = models.CharField(
verbose_name=_('label'),
max_length=64,
blank=True,
help_text=_("Physical label")
help_text=_('Physical label')
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
@ -96,7 +101,7 @@ class ComponentTemplateModel(ChangeLoggedModel):
if self.pk is not None and self._original_device_type != self.device_type_id:
raise ValidationError({
"device_type": "Component templates cannot be moved to a different device type."
"device_type": _("Component templates cannot be moved to a different device type.")
})
@ -147,11 +152,11 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
# A component template must belong to a DeviceType *or* to a ModuleType
if self.device_type and self.module_type:
raise ValidationError(
"A component template cannot be associated with both a device type and a module type."
_("A component template cannot be associated with both a device type and a module type.")
)
if not self.device_type and not self.module_type:
raise ValidationError(
"A component template must be associated with either a device type or a module type."
_("A component template must be associated with either a device type or a module type.")
)
def resolve_name(self, module):
@ -170,6 +175,7 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
A template for a ConsolePort to be created for a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=ConsolePortTypeChoices,
blank=True
@ -199,6 +205,7 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
A template for a ConsoleServerPort to be created for a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=ConsolePortTypeChoices,
blank=True
@ -229,21 +236,24 @@ class PowerPortTemplate(ModularComponentTemplateModel):
A template for a PowerPort to be created for a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PowerPortTypeChoices,
blank=True
)
maximum_draw = models.PositiveIntegerField(
verbose_name=_('maximum draw'),
blank=True,
null=True,
validators=[MinValueValidator(1)],
help_text=_("Maximum power draw (watts)")
help_text=_('Maximum power draw (watts)')
)
allocated_draw = models.PositiveIntegerField(
verbose_name=_('allocated draw'),
blank=True,
null=True,
validators=[MinValueValidator(1)],
help_text=_("Allocated power draw (watts)")
help_text=_('Allocated power draw (watts)')
)
component_model = PowerPort
@ -265,7 +275,7 @@ class PowerPortTemplate(ModularComponentTemplateModel):
if self.maximum_draw is not None and self.allocated_draw is not None:
if self.allocated_draw > self.maximum_draw:
raise ValidationError({
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
'allocated_draw': _("Allocated draw cannot exceed the maximum draw ({maximum_draw}W).").format(maximum_draw=self.maximum_draw)
})
def to_yaml(self):
@ -284,6 +294,7 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
A template for a PowerOutlet to be created for a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PowerOutletTypeChoices,
blank=True
@ -296,10 +307,11 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
related_name='poweroutlet_templates'
)
feed_leg = models.CharField(
verbose_name=_('feed leg'),
max_length=50,
choices=PowerOutletFeedLegChoices,
blank=True,
help_text=_("Phase (for three-phase feeds)")
help_text=_('Phase (for three-phase feeds)')
)
component_model = PowerOutlet
@ -311,11 +323,11 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
if self.power_port:
if self.device_type and self.power_port.device_type != self.device_type:
raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same device type"
_("Parent power port ({power_port}) must belong to the same device type").format(power_port=self.power_port)
)
if self.module_type and self.power_port.module_type != self.module_type:
raise ValidationError(
f"Parent power port ({self.power_port}) must belong to the same module type"
_("Parent power port ({power_port}) must belong to the same module type").format(power_port=self.power_port)
)
def instantiate(self, **kwargs):
@ -357,15 +369,17 @@ class InterfaceTemplate(ModularComponentTemplateModel):
blank=True
)
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=InterfaceTypeChoices
)
enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True
)
mgmt_only = models.BooleanField(
default=False,
verbose_name='Management only'
verbose_name=_('management only')
)
bridge = models.ForeignKey(
to='self',
@ -373,19 +387,25 @@ class InterfaceTemplate(ModularComponentTemplateModel):
related_name='bridge_interfaces',
null=True,
blank=True,
verbose_name='Bridge interface'
verbose_name=_('bridge interface')
)
poe_mode = models.CharField(
max_length=50,
choices=InterfacePoEModeChoices,
blank=True,
verbose_name='PoE mode'
verbose_name=_('PoE mode')
)
poe_type = models.CharField(
max_length=50,
choices=InterfacePoETypeChoices,
blank=True,
verbose_name='PoE type'
verbose_name=_('PoE type')
)
rf_role = models.CharField(
max_length=30,
choices=WirelessRoleChoices,
blank=True,
verbose_name=_('wireless role')
)
component_model = Interface
@ -395,14 +415,19 @@ class InterfaceTemplate(ModularComponentTemplateModel):
if self.bridge:
if self.pk and self.bridge_id == self.pk:
raise ValidationError({'bridge': "An interface cannot be bridged to itself."})
raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
if self.device_type and self.device_type != self.bridge.device_type:
raise ValidationError({
'bridge': f"Bridge interface ({self.bridge}) must belong to the same device type"
'bridge': _("Bridge interface ({bridge}) must belong to the same device type").format(bridge=self.bridge)
})
if self.module_type and self.module_type != self.bridge.module_type:
raise ValidationError({
'bridge': f"Bridge interface ({self.bridge}) must belong to the same module type"
'bridge': _("Bridge interface ({bridge}) must belong to the same module type").format(bridge=self.bridge)
})
if self.rf_role and self.type not in WIRELESS_IFACE_TYPES:
raise ValidationError({
'rf_role': "Wireless role may be set only on wireless interfaces."
})
def instantiate(self, **kwargs):
@ -414,6 +439,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
mgmt_only=self.mgmt_only,
poe_mode=self.poe_mode,
poe_type=self.poe_type,
rf_role=self.rf_role,
**kwargs
)
instantiate.do_not_call_in_templates = True
@ -429,6 +455,7 @@ class InterfaceTemplate(ModularComponentTemplateModel):
'bridge': self.bridge.name if self.bridge else None,
'poe_mode': self.poe_mode,
'poe_type': self.poe_type,
'rf_role': self.rf_role,
}
@ -437,10 +464,12 @@ class FrontPortTemplate(ModularComponentTemplateModel):
Template for a pass-through port on the front of a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PortTypeChoices
)
color = ColorField(
verbose_name=_('color'),
blank=True
)
rear_port = models.ForeignKey(
@ -449,6 +478,7 @@ class FrontPortTemplate(ModularComponentTemplateModel):
related_name='frontport_templates'
)
rear_port_position = models.PositiveSmallIntegerField(
verbose_name=_('rear port position'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
@ -482,13 +512,13 @@ class FrontPortTemplate(ModularComponentTemplateModel):
# Validate rear port assignment
if self.rear_port.device_type != self.device_type:
raise ValidationError(
"Rear port ({}) must belong to the same device type".format(self.rear_port)
_("Rear port ({}) must belong to the same device type").format(self.rear_port)
)
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError(
"Invalid rear port position ({}); rear port {} has only {} positions".format(
_("Invalid rear port position ({}); rear port {} has only {} positions").format(
self.rear_port_position, self.rear_port.name, self.rear_port.positions
)
)
@ -530,13 +560,16 @@ class RearPortTemplate(ModularComponentTemplateModel):
Template for a pass-through port on the rear of a new Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PortTypeChoices
)
color = ColorField(
verbose_name=_('color'),
blank=True
)
positions = models.PositiveSmallIntegerField(
verbose_name=_('positions'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
@ -573,6 +606,7 @@ class ModuleBayTemplate(ComponentTemplateModel):
A template for a ModuleBay to be created for a new parent Device.
"""
position = models.CharField(
verbose_name=_('position'),
max_length=30,
blank=True,
help_text=_('Identifier to reference when renaming installed components')
@ -615,7 +649,7 @@ class DeviceBayTemplate(ComponentTemplateModel):
def clean(self):
if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
raise ValidationError(
f"Subdevice role of device type ({self.device_type}) must be set to \"parent\" to allow device bays."
_("Subdevice role of device type ({device_type}) must be set to \"parent\" to allow device bays.").format(device_type=self.device_type)
)
def to_yaml(self):
@ -670,7 +704,7 @@ class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
)
part_id = models.CharField(
max_length=50,
verbose_name='Part ID',
verbose_name=_('part ID'),
blank=True,
help_text=_('Manufacturer-assigned part identifier')
)

View File

@ -7,7 +7,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Sum
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from mptt.models import MPTTModel, TreeForeignKey
from dcim.choices import *
@ -19,6 +19,7 @@ from utilities.fields import ColorField, NaturalOrderingField
from utilities.mptt import TreeManager
from utilities.ordering import naturalize_interface
from utilities.query_functions import CollateAsChar
from utilities.tracking import TrackingModelMixin
from wireless.choices import *
from wireless.utils import get_channel_attr
@ -51,6 +52,7 @@ class ComponentModel(NetBoxModel):
related_name='%(class)ss'
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
)
_name = NaturalOrderingField(
@ -59,11 +61,13 @@ class ComponentModel(NetBoxModel):
blank=True
)
label = models.CharField(
verbose_name=_('label'),
max_length=64,
blank=True,
help_text=_("Physical label")
help_text=_('Physical label')
)
description = models.CharField(
verbose_name=_('description'),
max_length=200,
blank=True
)
@ -100,7 +104,7 @@ class ComponentModel(NetBoxModel):
# Check list of Modules that allow device field to be changed
if (type(self) not in [InventoryItem]) and (self.pk is not None) and (self._original_device != self.device_id):
raise ValidationError({
"device": "Components cannot be moved to a different device."
"device": _("Components cannot be moved to a different device.")
})
@property
@ -139,13 +143,15 @@ class CabledObjectModel(models.Model):
null=True
)
cable_end = models.CharField(
verbose_name=_('cable end'),
max_length=1,
blank=True,
choices=CableEndChoices
)
mark_connected = models.BooleanField(
verbose_name=_('mark connected'),
default=False,
help_text=_("Treat as if a cable is connected")
help_text=_('Treat as if a cable is connected')
)
cable_terminations = GenericRelation(
@ -163,15 +169,15 @@ class CabledObjectModel(models.Model):
if self.cable and not self.cable_end:
raise ValidationError({
"cable_end": "Must specify cable end (A or B) when attaching a cable."
"cable_end": _("Must specify cable end (A or B) when attaching a cable.")
})
if self.cable_end and not self.cable:
raise ValidationError({
"cable_end": "Cable end must not be set without a cable."
"cable_end": _("Cable end must not be set without a cable.")
})
if self.mark_connected and self.cable:
raise ValidationError({
"mark_connected": "Cannot mark as connected with a cable attached."
"mark_connected": _("Cannot mark as connected with a cable attached.")
})
@property
@ -194,7 +200,9 @@ class CabledObjectModel(models.Model):
@property
def parent_object(self):
raise NotImplementedError(f"{self.__class__.__name__} models must declare a parent_object property")
raise NotImplementedError(
_("{class_name} models must declare a parent_object property").format(class_name=self.__class__.__name__)
)
@property
def opposite_cable_end(self):
@ -269,17 +277,19 @@ class PathEndpoint(models.Model):
# Console components
#
class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
"""
A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=ConsolePortTypeChoices,
blank=True,
help_text=_('Physical port type')
)
speed = models.PositiveIntegerField(
verbose_name=_('speed'),
choices=ConsolePortSpeedChoices,
blank=True,
null=True,
@ -292,17 +302,19 @@ class ConsolePort(ModularComponentModel, CabledObjectModel, PathEndpoint):
return reverse('dcim:consoleport', kwargs={'pk': self.pk})
class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
"""
A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=ConsolePortTypeChoices,
blank=True,
help_text=_('Physical port type')
)
speed = models.PositiveIntegerField(
verbose_name=_('speed'),
choices=ConsolePortSpeedChoices,
blank=True,
null=True,
@ -319,27 +331,30 @@ class ConsoleServerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
# Power components
#
class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
"""
A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PowerPortTypeChoices,
blank=True,
help_text=_('Physical port type')
)
maximum_draw = models.PositiveIntegerField(
verbose_name=_('maximum draw'),
blank=True,
null=True,
validators=[MinValueValidator(1)],
help_text=_("Maximum power draw (watts)")
)
allocated_draw = models.PositiveIntegerField(
verbose_name=_('allocated draw'),
blank=True,
null=True,
validators=[MinValueValidator(1)],
help_text=_("Allocated power draw (watts)")
help_text=_('Allocated power draw (watts)')
)
clone_fields = ('device', 'module', 'maximum_draw', 'allocated_draw')
@ -353,7 +368,9 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
if self.maximum_draw is not None and self.allocated_draw is not None:
if self.allocated_draw > self.maximum_draw:
raise ValidationError({
'allocated_draw': f"Allocated draw cannot exceed the maximum draw ({self.maximum_draw}W)."
'allocated_draw': _(
"Allocated draw cannot exceed the maximum draw ({maximum_draw}W)."
).format(maximum_draw=self.maximum_draw)
})
def get_downstream_powerports(self, leg=None):
@ -428,11 +445,12 @@ class PowerPort(ModularComponentModel, CabledObjectModel, PathEndpoint):
}
class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint, TrackingModelMixin):
"""
A physical power outlet (output) within a Device which provides power to a PowerPort.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PowerOutletTypeChoices,
blank=True,
@ -446,10 +464,11 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
related_name='poweroutlets'
)
feed_leg = models.CharField(
verbose_name=_('feed leg'),
max_length=50,
choices=PowerOutletFeedLegChoices,
blank=True,
help_text=_("Phase (for three-phase feeds)")
help_text=_('Phase (for three-phase feeds)')
)
clone_fields = ('device', 'module', 'type', 'power_port', 'feed_leg')
@ -462,7 +481,9 @@ class PowerOutlet(ModularComponentModel, CabledObjectModel, PathEndpoint):
# Validate power port assignment
if self.power_port and self.power_port.device != self.device:
raise ValidationError(f"Parent power port ({self.power_port}) must belong to the same device")
raise ValidationError(
_("Parent power port ({power_port}) must belong to the same device").format(power_port=self.power_port)
)
#
@ -474,12 +495,13 @@ class BaseInterface(models.Model):
Abstract base class for fields shared by dcim.Interface and virtualization.VMInterface.
"""
enabled = models.BooleanField(
verbose_name=_('enabled'),
default=True
)
mac_address = MACAddressField(
null=True,
blank=True,
verbose_name='MAC Address'
verbose_name=_('MAC address')
)
mtu = models.PositiveIntegerField(
blank=True,
@ -488,13 +510,14 @@ class BaseInterface(models.Model):
MinValueValidator(INTERFACE_MTU_MIN),
MaxValueValidator(INTERFACE_MTU_MAX)
],
verbose_name='MTU'
verbose_name=_('MTU')
)
mode = models.CharField(
verbose_name=_('mode'),
max_length=50,
choices=InterfaceModeChoices,
blank=True,
help_text=_("IEEE 802.1Q tagging strategy")
help_text=_('IEEE 802.1Q tagging strategy')
)
parent = models.ForeignKey(
to='self',
@ -502,7 +525,7 @@ class BaseInterface(models.Model):
related_name='child_interfaces',
null=True,
blank=True,
verbose_name='Parent interface'
verbose_name=_('parent interface')
)
bridge = models.ForeignKey(
to='self',
@ -510,7 +533,7 @@ class BaseInterface(models.Model):
related_name='bridge_interfaces',
null=True,
blank=True,
verbose_name='Bridge interface'
verbose_name=_('bridge interface')
)
class Meta:
@ -537,7 +560,7 @@ class BaseInterface(models.Model):
return self.fhrp_group_assignments.count()
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint):
class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEndpoint, TrackingModelMixin):
"""
A network interface within a Device. A physical Interface can connect to exactly one other Interface.
"""
@ -558,23 +581,25 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_name='member_interfaces',
null=True,
blank=True,
verbose_name='Parent LAG'
verbose_name=_('parent LAG')
)
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=InterfaceTypeChoices
)
mgmt_only = models.BooleanField(
default=False,
verbose_name='Management only',
verbose_name=_('management only'),
help_text=_('This interface is used only for out-of-band management')
)
speed = models.PositiveIntegerField(
blank=True,
null=True,
verbose_name='Speed (Kbps)'
verbose_name=_('speed (Kbps)')
)
duplex = models.CharField(
verbose_name=_('duplex'),
max_length=50,
blank=True,
null=True,
@ -583,27 +608,27 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
wwn = WWNField(
null=True,
blank=True,
verbose_name='WWN',
verbose_name=_('WWN'),
help_text=_('64-bit World Wide Name')
)
rf_role = models.CharField(
max_length=30,
choices=WirelessRoleChoices,
blank=True,
verbose_name='Wireless role'
verbose_name=_('wireless role')
)
rf_channel = models.CharField(
max_length=50,
choices=WirelessChannelChoices,
blank=True,
verbose_name='Wireless channel'
verbose_name=_('wireless channel')
)
rf_channel_frequency = models.DecimalField(
max_digits=7,
decimal_places=2,
blank=True,
null=True,
verbose_name='Channel frequency (MHz)',
verbose_name=_('channel frequency (MHz)'),
help_text=_("Populated by selected channel (if set)")
)
rf_channel_width = models.DecimalField(
@ -611,26 +636,26 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
decimal_places=3,
blank=True,
null=True,
verbose_name='Channel width (MHz)',
verbose_name=('channel width (MHz)'),
help_text=_("Populated by selected channel (if set)")
)
tx_power = models.PositiveSmallIntegerField(
blank=True,
null=True,
validators=(MaxValueValidator(127),),
verbose_name='Transmit power (dBm)'
verbose_name=_('transmit power (dBm)')
)
poe_mode = models.CharField(
max_length=50,
choices=InterfacePoEModeChoices,
blank=True,
verbose_name='PoE mode'
verbose_name=_('PoE mode')
)
poe_type = models.CharField(
max_length=50,
choices=InterfacePoETypeChoices,
blank=True,
verbose_name='PoE type'
verbose_name=_('PoE type')
)
wireless_link = models.ForeignKey(
to='wireless.WirelessLink',
@ -643,7 +668,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
to='wireless.WirelessLAN',
related_name='interfaces',
blank=True,
verbose_name='Wireless LANs'
verbose_name=_('wireless LANs')
)
untagged_vlan = models.ForeignKey(
to='ipam.VLAN',
@ -651,13 +676,13 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_name='interfaces_as_untagged',
null=True,
blank=True,
verbose_name='Untagged VLAN'
verbose_name=_('untagged VLAN')
)
tagged_vlans = models.ManyToManyField(
to='ipam.VLAN',
related_name='interfaces_as_tagged',
blank=True,
verbose_name='Tagged VLANs'
verbose_name=_('tagged VLANs')
)
vrf = models.ForeignKey(
to='ipam.VRF',
@ -665,7 +690,7 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
related_name='interfaces',
null=True,
blank=True,
verbose_name='VRF'
verbose_name=_('VRF')
)
ip_addresses = GenericRelation(
to='ipam.IPAddress',
@ -703,77 +728,98 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
# Virtual Interfaces cannot have a Cable attached
if self.is_virtual and self.cable:
raise ValidationError({
'type': f"{self.get_type_display()} interfaces cannot have a cable attached."
'type': _("{display_type} interfaces cannot have a cable attached.").format(
display_type=self.get_type_display()
)
})
# Virtual Interfaces cannot be marked as connected
if self.is_virtual and self.mark_connected:
raise ValidationError({
'mark_connected': f"{self.get_type_display()} interfaces cannot be marked as connected."
'mark_connected': _("{display_type} interfaces cannot be marked as connected.".format(
display_type=self.get_type_display())
)
})
# Parent validation
# An interface cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({'parent': "An interface cannot be its own parent."})
raise ValidationError({'parent': _("An interface cannot be its own parent.")})
# A physical interface cannot have a parent interface
if self.type != InterfaceTypeChoices.TYPE_VIRTUAL and self.parent is not None:
raise ValidationError({'parent': "Only virtual interfaces may be assigned to a parent interface."})
raise ValidationError({'parent': _("Only virtual interfaces may be assigned to a parent interface.")})
# An interface's parent must belong to the same device or virtual chassis
if self.parent and self.parent.device != self.device:
if self.device.virtual_chassis is None:
raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to a different device "
f"({self.parent.device})."
'parent': _(
"The selected parent interface ({interface}) belongs to a different device ({device})"
).format(interface=self.parent, device=self.parent.device)
})
elif self.parent.device.virtual_chassis != self.parent.virtual_chassis:
raise ValidationError({
'parent': f"The selected parent interface ({self.parent}) belongs to {self.parent.device}, which "
f"is not part of virtual chassis {self.device.virtual_chassis}."
'parent': _(
"The selected parent interface ({interface}) belongs to {device}, which is not part of "
"virtual chassis {virtual_chassis}."
).format(
interface=self.parent,
device=self.parent_device,
virtual_chassis=self.device.virtual_chassis
)
})
# Bridge validation
# An interface cannot be bridged to itself
if self.pk and self.bridge_id == self.pk:
raise ValidationError({'bridge': "An interface cannot be bridged to itself."})
raise ValidationError({'bridge': _("An interface cannot be bridged to itself.")})
# A bridged interface belong to the same device or virtual chassis
if self.bridge and self.bridge.device != self.device:
if self.device.virtual_chassis is None:
raise ValidationError({
'bridge': f"The selected bridge interface ({self.bridge}) belongs to a different device "
f"({self.bridge.device})."
'bridge': _("""
The selected bridge interface ({bridge}) belongs to a different device
({device}).""").format(bridge=self.bridge, device=self.bridge.device)
})
elif self.bridge.device.virtual_chassis != self.device.virtual_chassis:
raise ValidationError({
'bridge': f"The selected bridge interface ({self.bridge}) belongs to {self.bridge.device}, which "
f"is not part of virtual chassis {self.device.virtual_chassis}."
'bridge': _(
"The selected bridge interface ({interface}) belongs to {device}, which is not part of virtual "
"chassis {virtual_chassis}."
).format(
interface=self.bridge, device=self.bridge.device, virtual_chassis=self.device.virtual_chassis
)
})
# LAG validation
# A virtual interface cannot have a parent LAG
if self.type == InterfaceTypeChoices.TYPE_VIRTUAL and self.lag is not None:
raise ValidationError({'lag': "Virtual interfaces cannot have a parent LAG interface."})
raise ValidationError({'lag': _("Virtual interfaces cannot have a parent LAG interface.")})
# A LAG interface cannot be its own parent
if self.pk and self.lag_id == self.pk:
raise ValidationError({'lag': "A LAG interface cannot be its own parent."})
raise ValidationError({'lag': _("A LAG interface cannot be its own parent.")})
# An interface's LAG must belong to the same device or virtual chassis
if self.lag and self.lag.device != self.device:
if self.device.virtual_chassis is None:
raise ValidationError({
'lag': f"The selected LAG interface ({self.lag}) belongs to a different device ({self.lag.device})."
'lag': _(
"The selected LAG interface ({lag}) belongs to a different device ({device})."
).format(lag=self.lag, device=self.lag.device)
})
elif self.lag.device.virtual_chassis != self.device.virtual_chassis:
raise ValidationError({
'lag': f"The selected LAG interface ({self.lag}) belongs to {self.lag.device}, which is not part "
f"of virtual chassis {self.device.virtual_chassis}."
'lag': _(
"The selected LAG interface ({lag}) belongs to {device}, which is not part of virtual chassis "
"{virtual_chassis}.".format(
lag=self.lag, device=self.lag.device, virtual_chassis=self.device.virtual_chassis)
)
})
# PoE validation
@ -781,52 +827,54 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
# Only physical interfaces may have a PoE mode/type assigned
if self.poe_mode and self.is_virtual:
raise ValidationError({
'poe_mode': "Virtual interfaces cannot have a PoE mode."
'poe_mode': _("Virtual interfaces cannot have a PoE mode.")
})
if self.poe_type and self.is_virtual:
raise ValidationError({
'poe_type': "Virtual interfaces cannot have a PoE type."
'poe_type': _("Virtual interfaces cannot have a PoE type.")
})
# An interface with a PoE type set must also specify a mode
if self.poe_type and not self.poe_mode:
raise ValidationError({
'poe_type': "Must specify PoE mode when designating a PoE type."
'poe_type': _("Must specify PoE mode when designating a PoE type.")
})
# Wireless validation
# RF role & channel may only be set for wireless interfaces
if self.rf_role and not self.is_wireless:
raise ValidationError({'rf_role': "Wireless role may be set only on wireless interfaces."})
raise ValidationError({'rf_role': _("Wireless role may be set only on wireless interfaces.")})
if self.rf_channel and not self.is_wireless:
raise ValidationError({'rf_channel': "Channel may be set only on wireless interfaces."})
raise ValidationError({'rf_channel': _("Channel may be set only on wireless interfaces.")})
# Validate channel frequency against interface type and selected channel (if any)
if self.rf_channel_frequency:
if not self.is_wireless:
raise ValidationError({
'rf_channel_frequency': "Channel frequency may be set only on wireless interfaces.",
'rf_channel_frequency': _("Channel frequency may be set only on wireless interfaces."),
})
if self.rf_channel and self.rf_channel_frequency != get_channel_attr(self.rf_channel, 'frequency'):
raise ValidationError({
'rf_channel_frequency': "Cannot specify custom frequency with channel selected.",
'rf_channel_frequency': _("Cannot specify custom frequency with channel selected."),
})
# Validate channel width against interface type and selected channel (if any)
if self.rf_channel_width:
if not self.is_wireless:
raise ValidationError({'rf_channel_width': "Channel width may be set only on wireless interfaces."})
raise ValidationError({'rf_channel_width': _("Channel width may be set only on wireless interfaces.")})
if self.rf_channel and self.rf_channel_width != get_channel_attr(self.rf_channel, 'width'):
raise ValidationError({'rf_channel_width': "Cannot specify custom width with channel selected."})
raise ValidationError({'rf_channel_width': _("Cannot specify custom width with channel selected.")})
# VLAN validation
# Validate untagged VLAN
if self.untagged_vlan and self.untagged_vlan.site not in [self.device.site, None]:
raise ValidationError({
'untagged_vlan': f"The untagged VLAN ({self.untagged_vlan}) must belong to the same site as the "
f"interface's parent device, or it must be global."
'untagged_vlan': _("""
The untagged VLAN ({untagged_vlan}) must belong to the same site as the
interface's parent device, or it must be global.
""").format(untagged_vlan=self.untagged_vlan)
})
def save(self, *args, **kwargs):
@ -888,15 +936,17 @@ class Interface(ModularComponentModel, BaseInterface, CabledObjectModel, PathEnd
# Pass-through ports
#
class FrontPort(ModularComponentModel, CabledObjectModel):
class FrontPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
"""
A pass-through port on the front of a Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PortTypeChoices
)
color = ColorField(
verbose_name=_('color'),
blank=True
)
rear_port = models.ForeignKey(
@ -905,6 +955,7 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
related_name='frontports'
)
rear_port_position = models.PositiveSmallIntegerField(
verbose_name=_('rear port position'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
@ -938,29 +989,40 @@ class FrontPort(ModularComponentModel, CabledObjectModel):
# Validate rear port assignment
if self.rear_port.device != self.device:
raise ValidationError({
"rear_port": f"Rear port ({self.rear_port}) must belong to the same device"
"rear_port": _(
"Rear port ({rear_port}) must belong to the same device"
).format(rear_port=self.rear_port)
})
# Validate rear port position assignment
if self.rear_port_position > self.rear_port.positions:
raise ValidationError({
"rear_port_position": f"Invalid rear port position ({self.rear_port_position}): Rear port "
f"{self.rear_port.name} has only {self.rear_port.positions} positions"
"rear_port_position": _(
"Invalid rear port position ({rear_port_position}): Rear port {name} has only {positions} "
"positions."
).format(
rear_port_position=self.rear_port_position,
name=self.rear_port.name,
positions=self.rear_port.positions
)
})
class RearPort(ModularComponentModel, CabledObjectModel):
class RearPort(ModularComponentModel, CabledObjectModel, TrackingModelMixin):
"""
A pass-through port on the rear of a Device.
"""
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PortTypeChoices
)
color = ColorField(
verbose_name=_('color'),
blank=True
)
positions = models.PositiveSmallIntegerField(
verbose_name=_('positions'),
default=1,
validators=[
MinValueValidator(REARPORT_POSITIONS_MIN),
@ -981,8 +1043,9 @@ class RearPort(ModularComponentModel, CabledObjectModel):
frontport_count = self.frontports.count()
if self.positions < frontport_count:
raise ValidationError({
"positions": f"The number of positions cannot be less than the number of mapped front ports "
f"({frontport_count})"
"positions": _("""
The number of positions cannot be less than the number of mapped front ports
({frontport_count})""").format(frontport_count=frontport_count)
})
@ -990,11 +1053,12 @@ class RearPort(ModularComponentModel, CabledObjectModel):
# Bays
#
class ModuleBay(ComponentModel):
class ModuleBay(ComponentModel, TrackingModelMixin):
"""
An empty space within a Device which can house a child device
"""
position = models.CharField(
verbose_name=_('position'),
max_length=30,
blank=True,
help_text=_('Identifier to reference when renaming installed components')
@ -1006,14 +1070,14 @@ class ModuleBay(ComponentModel):
return reverse('dcim:modulebay', kwargs={'pk': self.pk})
class DeviceBay(ComponentModel):
class DeviceBay(ComponentModel, TrackingModelMixin):
"""
An empty space within a Device which can house a child device
"""
installed_device = models.OneToOneField(
to='dcim.Device',
on_delete=models.SET_NULL,
related_name='parent_bay',
related_name=_('parent_bay'),
blank=True,
null=True
)
@ -1028,22 +1092,22 @@ class DeviceBay(ComponentModel):
# Validate that the parent Device can have DeviceBays
if not self.device.device_type.is_parent_device:
raise ValidationError("This type of device ({}) does not support device bays.".format(
self.device.device_type
raise ValidationError(_("This type of device ({device_type}) does not support device bays.").format(
device_type=self.device.device_type
))
# Cannot install a device into itself, obviously
if self.device == self.installed_device:
raise ValidationError("Cannot install a device into itself.")
raise ValidationError(_("Cannot install a device into itself."))
# Check that the installed device is not already installed elsewhere
if self.installed_device:
current_bay = DeviceBay.objects.filter(installed_device=self.installed_device).first()
if current_bay and current_bay != self:
raise ValidationError({
'installed_device': "Cannot install the specified device; device is already installed in {}".format(
current_bay
)
'installed_device': _(
"Cannot install the specified device; device is already installed in {bay}."
).format(bay=current_bay)
})
@ -1057,6 +1121,7 @@ class InventoryItemRole(OrganizationalModel):
Inventory items may optionally be assigned a functional role.
"""
color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY
)
@ -1064,7 +1129,7 @@ class InventoryItemRole(OrganizationalModel):
return reverse('dcim:inventoryitemrole', args=[self.pk])
class InventoryItem(MPTTModel, ComponentModel):
class InventoryItem(MPTTModel, ComponentModel, TrackingModelMixin):
"""
An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
InventoryItems are used only for inventory purposes.
@ -1109,13 +1174,13 @@ class InventoryItem(MPTTModel, ComponentModel):
)
part_id = models.CharField(
max_length=50,
verbose_name='Part ID',
verbose_name=_('part ID'),
blank=True,
help_text=_('Manufacturer-assigned part identifier')
)
serial = models.CharField(
max_length=50,
verbose_name='Serial number',
verbose_name=_('serial number'),
blank=True
)
asset_tag = models.CharField(
@ -1123,10 +1188,11 @@ class InventoryItem(MPTTModel, ComponentModel):
unique=True,
blank=True,
null=True,
verbose_name='Asset tag',
verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this item')
)
discovered = models.BooleanField(
verbose_name=_('discovered'),
default=False,
help_text=_('This item was automatically discovered')
)
@ -1153,7 +1219,7 @@ class InventoryItem(MPTTModel, ComponentModel):
# An InventoryItem cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
"parent": _("Cannot assign self as parent.")
})
# Validation for moving InventoryItems
@ -1161,13 +1227,13 @@ class InventoryItem(MPTTModel, ComponentModel):
# Cannot move an InventoryItem to another device if it has a parent
if self.parent and self.parent.device != self.device:
raise ValidationError({
"parent": "Parent inventory item does not belong to the same device."
"parent": _("Parent inventory item does not belong to the same device.")
})
# Prevent moving InventoryItems with children
first_child = self.get_children().first()
if first_child and first_child.device != self.device:
raise ValidationError("Cannot move an inventory item with dependent children")
raise ValidationError(_("Cannot move an inventory item with dependent children"))
# When moving an InventoryItem to another device, remove any associated component
if self.component and self.component.device != self.device:
@ -1175,5 +1241,5 @@ class InventoryItem(MPTTModel, ComponentModel):
else:
if self.component and self.component.device != self.device:
raise ValidationError({
"device": "Cannot assign inventory item to component on another device"
"device": _("Cannot assign inventory item to component on another device")
})

View File

@ -12,7 +12,7 @@ from django.db.models.functions import Lower
from django.db.models.signals import post_save
from django.urls import reverse
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@ -21,7 +21,8 @@ from extras.querysets import ConfigContextModelQuerySet
from netbox.config import ConfigItem
from netbox.models import OrganizationalModel, PrimaryModel
from utilities.choices import ColorChoices
from utilities.fields import ColorField, NaturalOrderingField
from utilities.fields import ColorField, CounterCacheField, NaturalOrderingField
from utilities.tracking import TrackingModelMixin
from .device_components import *
from .mixins import WeightMixin
@ -77,9 +78,11 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='device_types'
)
model = models.CharField(
verbose_name=_('model'),
max_length=100
)
slug = models.SlugField(
verbose_name=_('slug'),
max_length=100
)
default_platform = models.ForeignKey(
@ -88,9 +91,10 @@ class DeviceType(PrimaryModel, WeightMixin):
related_name='+',
blank=True,
null=True,
verbose_name='Default platform'
verbose_name=_('default platform')
)
part_number = models.CharField(
verbose_name=_('part number'),
max_length=50,
blank=True,
help_text=_('Discrete part number (optional)')
@ -99,22 +103,23 @@ class DeviceType(PrimaryModel, WeightMixin):
max_digits=4,
decimal_places=1,
default=1.0,
verbose_name='Height (U)'
verbose_name=_('height (U)')
)
is_full_depth = models.BooleanField(
default=True,
verbose_name='Is full depth',
verbose_name=_('is full depth'),
help_text=_('Device consumes both front and rear rack faces')
)
subdevice_role = models.CharField(
max_length=50,
choices=SubdeviceRoleChoices,
blank=True,
verbose_name='Parent/child status',
verbose_name=_('parent/child status'),
help_text=_('Parent devices house child devices in device bays. Leave blank '
'if this device type is neither a parent nor a child.')
)
airflow = models.CharField(
verbose_name=_('airflow'),
max_length=50,
choices=DeviceAirflowChoices,
blank=True
@ -128,12 +133,55 @@ class DeviceType(PrimaryModel, WeightMixin):
blank=True
)
# Counter fields
console_port_template_count = CounterCacheField(
to_model='dcim.ConsolePortTemplate',
to_field='device_type'
)
console_server_port_template_count = CounterCacheField(
to_model='dcim.ConsoleServerPortTemplate',
to_field='device_type'
)
power_port_template_count = CounterCacheField(
to_model='dcim.PowerPortTemplate',
to_field='device_type'
)
power_outlet_template_count = CounterCacheField(
to_model='dcim.PowerOutletTemplate',
to_field='device_type'
)
interface_template_count = CounterCacheField(
to_model='dcim.InterfaceTemplate',
to_field='device_type'
)
front_port_template_count = CounterCacheField(
to_model='dcim.FrontPortTemplate',
to_field='device_type'
)
rear_port_template_count = CounterCacheField(
to_model='dcim.RearPortTemplate',
to_field='device_type'
)
device_bay_template_count = CounterCacheField(
to_model='dcim.DeviceBayTemplate',
to_field='device_type'
)
module_bay_template_count = CounterCacheField(
to_model='dcim.ModuleBayTemplate',
to_field='device_type'
)
inventory_item_template_count = CounterCacheField(
to_model='dcim.InventoryItemTemplate',
to_field='device_type'
)
images = GenericRelation(
to='extras.ImageAttachment'
)
clone_fields = (
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight', 'weight_unit'
'manufacturer', 'default_platform', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'weight',
'weight_unit',
)
prerequisite_models = (
'dcim.Manufacturer',
@ -232,9 +280,9 @@ class DeviceType(PrimaryModel, WeightMixin):
super().clean()
# U height must be divisible by 0.5
if self.u_height % decimal.Decimal(0.5):
if decimal.Decimal(self.u_height) % decimal.Decimal(0.5):
raise ValidationError({
'u_height': "U height must be in increments of 0.5 rack units."
'u_height': _("U height must be in increments of 0.5 rack units.")
})
# If editing an existing DeviceType to have a larger u_height, first validate that *all* instances of it have
@ -250,8 +298,8 @@ class DeviceType(PrimaryModel, WeightMixin):
)
if d.position not in u_available:
raise ValidationError({
'u_height': "Device {} in rack {} does not have sufficient space to accommodate a height of "
"{}U".format(d, d.rack, self.u_height)
'u_height': _("Device {} in rack {} does not have sufficient space to accommodate a height of "
"{}U").format(d, d.rack, self.u_height)
})
# If modifying the height of an existing DeviceType to 0U, check for any instances assigned to a rack position.
@ -263,23 +311,23 @@ class DeviceType(PrimaryModel, WeightMixin):
if racked_instance_count:
url = f"{reverse('dcim:device_list')}?manufactuer_id={self.manufacturer_id}&device_type_id={self.pk}"
raise ValidationError({
'u_height': mark_safe(
f'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
f'mounted within racks.'
)
'u_height': mark_safe(_(
'Unable to set 0U height: Found <a href="{url}">{racked_instance_count} instances</a> already '
'mounted within racks.'
).format(url=url, racked_instance_count=racked_instance_count))
})
if (
self.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT
) and self.pk and self.devicebaytemplates.count():
raise ValidationError({
'subdevice_role': "Must delete all device bay templates associated with this device before "
"declassifying it as a parent device."
'subdevice_role': _("Must delete all device bay templates associated with this device before "
"declassifying it as a parent device.")
})
if self.u_height and self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD:
raise ValidationError({
'u_height': "Child device types must be 0U."
'u_height': _("Child device types must be 0U.")
})
def save(self, *args, **kwargs):
@ -324,9 +372,11 @@ class ModuleType(PrimaryModel, WeightMixin):
related_name='module_types'
)
model = models.CharField(
verbose_name=_('model'),
max_length=100
)
part_number = models.CharField(
verbose_name=_('part number'),
max_length=50,
blank=True,
help_text=_('Discrete part number (optional)')
@ -411,11 +461,12 @@ class DeviceRole(OrganizationalModel):
virtual machines as well.
"""
color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY
)
vm_role = models.BooleanField(
default=True,
verbose_name='VM Role',
verbose_name=_('VM role'),
help_text=_('Virtual machines may be assigned to this role')
)
config_template = models.ForeignKey(
@ -469,7 +520,7 @@ def update_interface_bridges(device, interface_templates, module=None):
interface.save()
class Device(PrimaryModel, ConfigContextModel):
class Device(PrimaryModel, ConfigContextModel, TrackingModelMixin):
"""
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,
DeviceRole, and (optionally) a Platform. Device names are not required, however if one is set it must be unique.
@ -507,6 +558,7 @@ class Device(PrimaryModel, ConfigContextModel):
null=True
)
name = models.CharField(
verbose_name=_('name'),
max_length=64,
blank=True,
null=True
@ -520,7 +572,7 @@ class Device(PrimaryModel, ConfigContextModel):
serial = models.CharField(
max_length=50,
blank=True,
verbose_name='Serial number',
verbose_name=_('serial number'),
help_text=_("Chassis serial number, assigned by the manufacturer")
)
asset_tag = models.CharField(
@ -528,7 +580,7 @@ class Device(PrimaryModel, ConfigContextModel):
blank=True,
null=True,
unique=True,
verbose_name='Asset tag',
verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this device')
)
site = models.ForeignKey(
@ -556,21 +608,23 @@ class Device(PrimaryModel, ConfigContextModel):
blank=True,
null=True,
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX + 0.5)],
verbose_name='Position (U)',
verbose_name=_('position (U)'),
help_text=_('The lowest-numbered unit occupied by the device')
)
face = models.CharField(
max_length=50,
blank=True,
choices=DeviceFaceChoices,
verbose_name='Rack face'
verbose_name=_('rack face')
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=DeviceStatusChoices,
default=DeviceStatusChoices.STATUS_ACTIVE
)
airflow = models.CharField(
verbose_name=_('airflow'),
max_length=50,
choices=DeviceAirflowChoices,
blank=True
@ -581,7 +635,7 @@ class Device(PrimaryModel, ConfigContextModel):
related_name='+',
blank=True,
null=True,
verbose_name='Primary IPv4'
verbose_name=_('primary IPv4')
)
primary_ip6 = models.OneToOneField(
to='ipam.IPAddress',
@ -589,7 +643,15 @@ class Device(PrimaryModel, ConfigContextModel):
related_name='+',
blank=True,
null=True,
verbose_name='Primary IPv6'
verbose_name=_('primary IPv6')
)
oob_ip = models.OneToOneField(
to='ipam.IPAddress',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
verbose_name=_('out-of-band IP')
)
cluster = models.ForeignKey(
to='virtualization.Cluster',
@ -606,12 +668,14 @@ class Device(PrimaryModel, ConfigContextModel):
null=True
)
vc_position = models.PositiveSmallIntegerField(
verbose_name=_('VC position'),
blank=True,
null=True,
validators=[MaxValueValidator(255)],
help_text=_('Virtual chassis position')
)
vc_priority = models.PositiveSmallIntegerField(
verbose_name=_('VC priority'),
blank=True,
null=True,
validators=[MaxValueValidator(255)],
@ -625,6 +689,7 @@ class Device(PrimaryModel, ConfigContextModel):
null=True
)
latitude = models.DecimalField(
verbose_name=_('latitude'),
max_digits=8,
decimal_places=6,
blank=True,
@ -632,6 +697,7 @@ class Device(PrimaryModel, ConfigContextModel):
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
)
longitude = models.DecimalField(
verbose_name=_('longitude'),
max_digits=9,
decimal_places=6,
blank=True,
@ -639,6 +705,48 @@ class Device(PrimaryModel, ConfigContextModel):
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
)
# Counter fields
console_port_count = CounterCacheField(
to_model='dcim.ConsolePort',
to_field='device'
)
console_server_port_count = CounterCacheField(
to_model='dcim.ConsoleServerPort',
to_field='device'
)
power_port_count = CounterCacheField(
to_model='dcim.PowerPort',
to_field='device'
)
power_outlet_count = CounterCacheField(
to_model='dcim.PowerOutlet',
to_field='device'
)
interface_count = CounterCacheField(
to_model='dcim.Interface',
to_field='device'
)
front_port_count = CounterCacheField(
to_model='dcim.FrontPort',
to_field='device'
)
rear_port_count = CounterCacheField(
to_model='dcim.RearPort',
to_field='device'
)
device_bay_count = CounterCacheField(
to_model='dcim.DeviceBay',
to_field='device'
)
module_bay_count = CounterCacheField(
to_model='dcim.ModuleBay',
to_field='device'
)
inventory_item_count = CounterCacheField(
to_model='dcim.InventoryItem',
to_field='device'
)
# Generic relations
contacts = GenericRelation(
to='tenancy.ContactAssignment'
@ -670,7 +778,7 @@ class Device(PrimaryModel, ConfigContextModel):
Lower('name'), 'site',
name='%(app_label)s_%(class)s_unique_name_site',
condition=Q(tenant__isnull=True),
violation_error_message="Device name must be unique per site."
violation_error_message=_("Device name must be unique per site.")
),
models.UniqueConstraint(
fields=('rack', 'position', 'face'),
@ -706,42 +814,48 @@ class Device(PrimaryModel, ConfigContextModel):
# Validate site/location/rack combination
if self.rack and self.site != self.rack.site:
raise ValidationError({
'rack': f"Rack {self.rack} does not belong to site {self.site}.",
'rack': _("Rack {rack} does not belong to site {site}.").format(rack=self.rack, site=self.site),
})
if self.location and self.site != self.location.site:
raise ValidationError({
'location': f"Location {self.location} does not belong to site {self.site}.",
'location': _(
"Location {location} does not belong to site {site}."
).format(location=self.location, site=self.site)
})
if self.rack and self.location and self.rack.location != self.location:
raise ValidationError({
'rack': f"Rack {self.rack} does not belong to location {self.location}.",
'rack': _(
"Rack {rack} does not belong to location {location}."
).format(rack=self.rack, location=self.location)
})
if self.rack is None:
if self.face:
raise ValidationError({
'face': "Cannot select a rack face without assigning a rack.",
'face': _("Cannot select a rack face without assigning a rack."),
})
if self.position:
raise ValidationError({
'position': "Cannot select a rack position without assigning a rack.",
'position': _("Cannot select a rack position without assigning a rack."),
})
# Validate rack position and face
if self.position and self.position % decimal.Decimal(0.5):
raise ValidationError({
'position': "Position must be in increments of 0.5 rack units."
'position': _("Position must be in increments of 0.5 rack units.")
})
if self.position and not self.face:
raise ValidationError({
'face': "Must specify rack face when defining rack position.",
'face': _("Must specify rack face when defining rack position."),
})
# Prevent 0U devices from being assigned to a specific position
if hasattr(self, 'device_type'):
if self.position and self.device_type.u_height == 0:
raise ValidationError({
'position': f"A U0 device type ({self.device_type}) cannot be assigned to a rack position."
'position': _(
"A U0 device type ({device_type}) cannot be assigned to a rack position."
).format(device_type=self.device_type)
})
if self.rack:
@ -750,13 +864,17 @@ class Device(PrimaryModel, ConfigContextModel):
# Child devices cannot be assigned to a rack face/unit
if self.device_type.is_child_device and self.face:
raise ValidationError({
'face': "Child device types cannot be assigned to a rack face. This is an attribute of the "
"parent device."
'face': _(
"Child device types cannot be assigned to a rack face. This is an attribute of the parent "
"device."
)
})
if self.device_type.is_child_device and self.position:
raise ValidationError({
'position': "Child device types cannot be assigned to a rack position. This is an attribute of "
"the parent device."
'position': _(
"Child device types cannot be assigned to a rack position. This is an attribute of the "
"parent device."
)
})
# Validate rack space
@ -767,19 +885,23 @@ class Device(PrimaryModel, ConfigContextModel):
)
if self.position and self.position not in available_units:
raise ValidationError({
'position': f"U{self.position} is already occupied or does not have sufficient space to "
f"accommodate this device type: {self.device_type} ({self.device_type.u_height}U)"
'position': _(
"U{position} is already occupied or does not have sufficient space to accommodate this "
"device type: {device_type} ({u_height}U)"
).format(
position=self.position, device_type=self.device_type, u_height=self.device_type.u_height
)
})
except DeviceType.DoesNotExist:
pass
# Validate primary IP addresses
# Validate primary & OOB IP addresses
vc_interfaces = self.vc_interfaces(if_master=False)
if self.primary_ip4:
if self.primary_ip4.family != 4:
raise ValidationError({
'primary_ip4': f"{self.primary_ip4} is not an IPv4 address."
'primary_ip4': _("{primary_ip4} is not an IPv4 address.").format(primary_ip4=self.primary_ip4)
})
if self.primary_ip4.assigned_object in vc_interfaces:
pass
@ -787,12 +909,14 @@ class Device(PrimaryModel, ConfigContextModel):
pass
else:
raise ValidationError({
'primary_ip4': f"The specified IP address ({self.primary_ip4}) is not assigned to this device."
'primary_ip4': _(
"The specified IP address ({primary_ip4}) is not assigned to this device."
).format(primary_ip4=self.primary_ip4)
})
if self.primary_ip6:
if self.primary_ip6.family != 6:
raise ValidationError({
'primary_ip6': f"{self.primary_ip6} is not an IPv6 address."
'primary_ip6': _("{primary_ip6} is not an IPv6 address.").format(primary_ip6=self.primary_ip6m)
})
if self.primary_ip6.assigned_object in vc_interfaces:
pass
@ -800,27 +924,43 @@ class Device(PrimaryModel, ConfigContextModel):
pass
else:
raise ValidationError({
'primary_ip6': f"The specified IP address ({self.primary_ip6}) is not assigned to this device."
'primary_ip6': _(
"The specified IP address ({primary_ip6}) is not assigned to this device."
).format(primary_ip6=self.primary_ip6)
})
if self.oob_ip:
if self.oob_ip.assigned_object in vc_interfaces:
pass
elif self.oob_ip.nat_inside is not None and self.oob_ip.nat_inside.assigned_object in vc_interfaces:
pass
else:
raise ValidationError({
'oob_ip': f"The specified IP address ({self.oob_ip}) is not assigned to this device."
})
# Validate manufacturer/platform
if hasattr(self, 'device_type') and self.platform:
if self.platform.manufacturer and self.platform.manufacturer != self.device_type.manufacturer:
raise ValidationError({
'platform': f"The assigned platform is limited to {self.platform.manufacturer} device types, but "
f"this device's type belongs to {self.device_type.manufacturer}."
'platform': _(
"The assigned platform is limited to {platform_manufacturer} device types, but this device's "
"type belongs to {device_type_manufacturer}."
).format(
platform_manufacturer=self.platform.manufacturer,
device_type_manufacturer=self.device_type.manufacturer
)
})
# A Device can only be assigned to a Cluster in the same Site (or no Site)
if self.cluster and self.cluster.site is not None and self.cluster.site != self.site:
raise ValidationError({
'cluster': "The assigned cluster belongs to a different site ({})".format(self.cluster.site)
'cluster': _("The assigned cluster belongs to a different site ({})").format(self.cluster.site)
})
# Validate virtual chassis assignment
if self.virtual_chassis and self.vc_position is None:
raise ValidationError({
'vc_position': "A device assigned to a virtual chassis must have its position defined."
'vc_position': _("A device assigned to a virtual chassis must have its position defined.")
})
def _instantiate_components(self, queryset, bulk_create=True):
@ -1005,6 +1145,7 @@ class Module(PrimaryModel, ConfigContextModel):
related_name='instances'
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=ModuleStatusChoices,
default=ModuleStatusChoices.STATUS_ACTIVE
@ -1012,14 +1153,14 @@ class Module(PrimaryModel, ConfigContextModel):
serial = models.CharField(
max_length=50,
blank=True,
verbose_name='Serial number'
verbose_name=_('serial number')
)
asset_tag = models.CharField(
max_length=50,
blank=True,
null=True,
unique=True,
verbose_name='Asset tag',
verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this device')
)
@ -1042,7 +1183,9 @@ class Module(PrimaryModel, ConfigContextModel):
if hasattr(self, "module_bay") and (self.module_bay.device != self.device):
raise ValidationError(
f"Module must be installed within a module bay belonging to the assigned device ({self.device})."
_("Module must be installed within a module bay belonging to the assigned device ({device}).").format(
device=self.device
)
)
def save(self, *args, **kwargs):
@ -1140,13 +1283,21 @@ class VirtualChassis(PrimaryModel):
null=True
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
)
domain = models.CharField(
verbose_name=_('domain'),
max_length=30,
blank=True
)
# Counter fields
member_count = CounterCacheField(
to_model='dcim.Device',
to_field='virtual_chassis'
)
class Meta:
ordering = ['name']
verbose_name_plural = 'virtual chassis'
@ -1164,7 +1315,9 @@ class VirtualChassis(PrimaryModel):
# VirtualChassis.)
if self.pk and self.master and self.master not in self.members.all():
raise ValidationError({
'master': f"The selected master ({self.master}) is not assigned to this virtual chassis."
'master': _("The selected master ({master}) is not assigned to this virtual chassis.").format(
master=self.master
)
})
def delete(self, *args, **kwargs):
@ -1177,10 +1330,10 @@ class VirtualChassis(PrimaryModel):
lag__device=F('device')
)
if interfaces:
raise ProtectedError(
f"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG",
interfaces
)
raise ProtectedError(_(
"Unable to delete virtual chassis {self}. There are member interfaces which form a cross-chassis LAG "
"interfaces."
).format(self=self, interfaces=InterfaceSpeedChoices))
return super().delete(*args, **kwargs)
@ -1194,14 +1347,17 @@ class VirtualDeviceContext(PrimaryModel):
null=True
)
name = models.CharField(
verbose_name=_('name'),
max_length=64
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=VirtualDeviceContextStatusChoices,
)
identifier = models.PositiveSmallIntegerField(
help_text='Numeric identifier unique to the parent device',
verbose_name=_('identifier'),
help_text=_('Numeric identifier unique to the parent device'),
blank=True,
null=True,
)
@ -1211,7 +1367,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+',
blank=True,
null=True,
verbose_name='Primary IPv4'
verbose_name=_('primary IPv4')
)
primary_ip6 = models.OneToOneField(
to='ipam.IPAddress',
@ -1219,7 +1375,7 @@ class VirtualDeviceContext(PrimaryModel):
related_name='+',
blank=True,
null=True,
verbose_name='Primary IPv6'
verbose_name=_('primary IPv6')
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
@ -1229,6 +1385,7 @@ class VirtualDeviceContext(PrimaryModel):
null=True
)
comments = models.TextField(
verbose_name=_('comments'),
blank=True
)
@ -1274,7 +1431,9 @@ class VirtualDeviceContext(PrimaryModel):
continue
if primary_ip.family != family:
raise ValidationError({
f'primary_ip{family}': f"{primary_ip} is not an IPv{family} address."
f'primary_ip{family}': _(
"{primary_ip} is not an IPv{family} address."
).format(family=family, primary_ip=primary_ip)
})
device_interfaces = self.device.vc_interfaces(if_master=False)
if primary_ip.assigned_object not in device_interfaces:

View File

@ -1,17 +1,20 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from utilities.utils import to_grams
class WeightMixin(models.Model):
weight = models.DecimalField(
verbose_name=_('weight'),
max_digits=8,
decimal_places=2,
blank=True,
null=True
)
weight_unit = models.CharField(
verbose_name=_('weight unit'),
max_length=50,
choices=WeightUnitChoices,
blank=True,
@ -40,4 +43,4 @@ class WeightMixin(models.Model):
# Validate weight and weight_unit
if self.weight and not self.weight_unit:
raise ValidationError("Must specify a unit when setting a weight")
raise ValidationError(_("Must specify a unit when setting a weight"))

View File

@ -3,7 +3,7 @@ from django.core.exceptions import ValidationError
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from netbox.config import ConfigItem
@ -36,6 +36,7 @@ class PowerPanel(PrimaryModel):
null=True
)
name = models.CharField(
verbose_name=_('name'),
max_length=100
)
@ -72,7 +73,8 @@ class PowerPanel(PrimaryModel):
# Location must belong to assigned Site
if self.location and self.location.site != self.site:
raise ValidationError(
f"Location {self.location} ({self.location.site}) is in a different site than {self.site}"
_("Location {location} ({location_site}) is in a different site than {site}").format(
location=self.location, location_site=self.location.site, site=self.site)
)
@ -92,49 +94,65 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
null=True
)
name = models.CharField(
verbose_name=_('name'),
max_length=100
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=PowerFeedStatusChoices,
default=PowerFeedStatusChoices.STATUS_ACTIVE
)
type = models.CharField(
verbose_name=_('type'),
max_length=50,
choices=PowerFeedTypeChoices,
default=PowerFeedTypeChoices.TYPE_PRIMARY
)
supply = models.CharField(
verbose_name=_('supply'),
max_length=50,
choices=PowerFeedSupplyChoices,
default=PowerFeedSupplyChoices.SUPPLY_AC
)
phase = models.CharField(
verbose_name=_('phase'),
max_length=50,
choices=PowerFeedPhaseChoices,
default=PowerFeedPhaseChoices.PHASE_SINGLE
)
voltage = models.SmallIntegerField(
verbose_name=_('voltage'),
default=ConfigItem('POWERFEED_DEFAULT_VOLTAGE'),
validators=[ExclusionValidator([0])]
)
amperage = models.PositiveSmallIntegerField(
verbose_name=_('amperage'),
validators=[MinValueValidator(1)],
default=ConfigItem('POWERFEED_DEFAULT_AMPERAGE')
)
max_utilization = models.PositiveSmallIntegerField(
verbose_name=_('max utilization'),
validators=[MinValueValidator(1), MaxValueValidator(100)],
default=ConfigItem('POWERFEED_DEFAULT_MAX_UTILIZATION'),
help_text=_("Maximum permissible draw (percentage)")
)
available_power = models.PositiveIntegerField(
verbose_name=_('available power'),
default=0,
editable=False
)
tenant = models.ForeignKey(
to='tenancy.Tenant',
on_delete=models.PROTECT,
related_name='power_feeds',
blank=True,
null=True
)
clone_fields = (
'power_panel', 'rack', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage',
'max_utilization',
'max_utilization', 'tenant',
)
prerequisite_models = (
'dcim.PowerPanel',
@ -160,14 +178,14 @@ class PowerFeed(PrimaryModel, PathEndpoint, CabledObjectModel):
# Rack must belong to same Site as PowerPanel
if self.rack and self.rack.site != self.power_panel.site:
raise ValidationError("Rack {} ({}) and power panel {} ({}) are in different sites".format(
raise ValidationError(_("Rack {} ({}) and power panel {} ({}) are in different sites").format(
self.rack, self.rack.site, self.power_panel, self.power_panel.site
))
# AC voltage cannot be negative
if self.voltage < 0 and self.supply == PowerFeedSupplyChoices.SUPPLY_AC:
raise ValidationError({
"voltage": "Voltage cannot be negative for AC supply"
"voltage": _("Voltage cannot be negative for AC supply")
})
def save(self, *args, **kwargs):

View File

@ -9,7 +9,7 @@ from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.db.models import Count
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from dcim.choices import *
from dcim.constants import *
@ -39,6 +39,7 @@ class RackRole(OrganizationalModel):
Racks can be organized by functional role, similar to Devices.
"""
color = ColorField(
verbose_name=_('color'),
default=ColorChoices.COLOR_GREY
)
@ -52,6 +53,7 @@ class Rack(PrimaryModel, WeightMixin):
Each Rack is assigned to a Site and (optionally) a Location.
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100
)
_name = NaturalOrderingField(
@ -63,7 +65,7 @@ class Rack(PrimaryModel, WeightMixin):
max_length=50,
blank=True,
null=True,
verbose_name='Facility ID',
verbose_name=_('facility ID'),
help_text=_("Locally-assigned identifier")
)
site = models.ForeignKey(
@ -86,6 +88,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=RackStatusChoices,
default=RackStatusChoices.STATUS_ACTIVE
@ -101,60 +104,64 @@ class Rack(PrimaryModel, WeightMixin):
serial = models.CharField(
max_length=50,
blank=True,
verbose_name='Serial number'
verbose_name=_('serial number')
)
asset_tag = models.CharField(
max_length=50,
blank=True,
null=True,
unique=True,
verbose_name='Asset tag',
verbose_name=_('asset tag'),
help_text=_('A unique tag used to identify this rack')
)
type = models.CharField(
choices=RackTypeChoices,
max_length=50,
blank=True,
verbose_name='Type'
verbose_name=_('type')
)
width = models.PositiveSmallIntegerField(
choices=RackWidthChoices,
default=RackWidthChoices.WIDTH_19IN,
verbose_name='Width',
verbose_name=_('width'),
help_text=_('Rail-to-rail width')
)
u_height = models.PositiveSmallIntegerField(
default=RACK_U_HEIGHT_DEFAULT,
verbose_name='Height (U)',
verbose_name=_('height (U)'),
validators=[MinValueValidator(1), MaxValueValidator(RACK_U_HEIGHT_MAX)],
help_text=_('Height in rack units')
)
starting_unit = models.PositiveSmallIntegerField(
default=RACK_STARTING_UNIT_DEFAULT,
verbose_name='Starting unit',
verbose_name=_('starting unit'),
help_text=_('Starting unit for rack')
)
desc_units = models.BooleanField(
default=False,
verbose_name='Descending units',
verbose_name=_('descending units'),
help_text=_('Units are numbered top-to-bottom')
)
outer_width = models.PositiveSmallIntegerField(
verbose_name=_('outer width'),
blank=True,
null=True,
help_text=_('Outer dimension of rack (width)')
)
outer_depth = models.PositiveSmallIntegerField(
verbose_name=_('outer depth'),
blank=True,
null=True,
help_text=_('Outer dimension of rack (depth)')
)
outer_unit = models.CharField(
verbose_name=_('outer unit'),
max_length=50,
choices=RackDimensionUnitChoices,
blank=True,
)
max_weight = models.PositiveIntegerField(
verbose_name=_('max weight'),
blank=True,
null=True,
help_text=_('Maximum load capacity for the rack')
@ -165,6 +172,7 @@ class Rack(PrimaryModel, WeightMixin):
null=True
)
mounting_depth = models.PositiveSmallIntegerField(
verbose_name=_('mounting depth'),
blank=True,
null=True,
help_text=(
@ -222,15 +230,15 @@ class Rack(PrimaryModel, WeightMixin):
# Validate location/site assignment
if self.site and self.location and self.location.site != self.site:
raise ValidationError(f"Assigned location must belong to parent site ({self.site}).")
raise ValidationError(_("Assigned location must belong to parent site ({site}).").format(site=self.site))
# Validate outer dimensions and unit
if (self.outer_width is not None or self.outer_depth is not None) and not self.outer_unit:
raise ValidationError("Must specify a unit when setting an outer width/depth")
raise ValidationError(_("Must specify a unit when setting an outer width/depth"))
# Validate max_weight and weight_unit
if self.max_weight and not self.weight_unit:
raise ValidationError("Must specify a unit when setting a maximum weight")
raise ValidationError(_("Must specify a unit when setting a maximum weight"))
if self.pk:
mounted_devices = Device.objects.filter(rack=self).exclude(position__isnull=True).order_by('position')
@ -240,22 +248,22 @@ class Rack(PrimaryModel, WeightMixin):
min_height = top_device.position + top_device.device_type.u_height - self.starting_unit
if self.u_height < min_height:
raise ValidationError({
'u_height': f"Rack must be at least {min_height}U tall to house currently installed devices."
'u_height': _("Rack must be at least {min_height}U tall to house currently installed devices.").format(min_height=min_height)
})
# Validate that the Rack's starting unit is less than or equal to the position of the lowest mounted Device
if last_device := mounted_devices.first():
if self.starting_unit > last_device.position:
raise ValidationError({
'starting_unit': f"Rack unit numbering must begin at {last_device.position} or less to house "
f"currently installed devices."
'starting_unit': _("Rack unit numbering must begin at {position} or less to house "
"currently installed devices.").format(position=last_device.position)
})
# Validate that Rack was assigned a Location of its same site, if applicable
if self.location:
if self.location.site != self.site:
raise ValidationError({
'location': f"Location must be from the same site, {self.site}."
'location': _("Location must be from the same site, {site}.").format(site=self.site)
})
def save(self, *args, **kwargs):
@ -504,6 +512,7 @@ class RackReservation(PrimaryModel):
related_name='reservations'
)
units = ArrayField(
verbose_name=_('units'),
base_field=models.PositiveSmallIntegerField()
)
tenant = models.ForeignKey(
@ -518,6 +527,7 @@ class RackReservation(PrimaryModel):
on_delete=models.PROTECT
)
description = models.CharField(
verbose_name=_('description'),
max_length=200
)
@ -544,7 +554,7 @@ class RackReservation(PrimaryModel):
invalid_units = [u for u in self.units if u not in self.rack.units]
if invalid_units:
raise ValidationError({
'units': "Invalid unit(s) for {}U rack: {}".format(
'units': _("Invalid unit(s) for {}U rack: {}").format(
self.rack.u_height,
', '.join([str(u) for u in invalid_units]),
),
@ -557,7 +567,7 @@ class RackReservation(PrimaryModel):
conflicting_units = [u for u in self.units if u in reserved_units]
if conflicting_units:
raise ValidationError({
'units': 'The following units have already been reserved: {}'.format(
'units': _('The following units have already been reserved: {}').format(
', '.join([str(u) for u in conflicting_units]),
)
})

View File

@ -2,7 +2,7 @@ from django.contrib.contenttypes.fields import GenericRelation
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from timezone_field import TimeZoneField
from dcim.choices import *
@ -49,7 +49,7 @@ class Region(NestedGroupModel):
fields=('name',),
name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True),
violation_error_message="A top-level region with this name already exists."
violation_error_message=_("A top-level region with this name already exists.")
),
models.UniqueConstraint(
fields=('parent', 'slug'),
@ -59,7 +59,7 @@ class Region(NestedGroupModel):
fields=('slug',),
name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True),
violation_error_message="A top-level region with this slug already exists."
violation_error_message=_("A top-level region with this slug already exists.")
),
)
@ -104,7 +104,7 @@ class SiteGroup(NestedGroupModel):
fields=('name',),
name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True),
violation_error_message="A top-level site group with this name already exists."
violation_error_message=_("A top-level site group with this name already exists.")
),
models.UniqueConstraint(
fields=('parent', 'slug'),
@ -114,7 +114,7 @@ class SiteGroup(NestedGroupModel):
fields=('slug',),
name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True),
violation_error_message="A top-level site group with this slug already exists."
violation_error_message=_("A top-level site group with this slug already exists.")
),
)
@ -138,6 +138,7 @@ class Site(PrimaryModel):
field can be used to include an external designation, such as a data center name (e.g. Equinix SV6).
"""
name = models.CharField(
verbose_name=_('name'),
max_length=100,
unique=True,
help_text=_("Full name of the site")
@ -148,10 +149,12 @@ class Site(PrimaryModel):
blank=True
)
slug = models.SlugField(
verbose_name=_('slug'),
max_length=100,
unique=True
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=SiteStatusChoices,
default=SiteStatusChoices.STATUS_ACTIVE
@ -178,9 +181,10 @@ class Site(PrimaryModel):
null=True
)
facility = models.CharField(
verbose_name=_('facility'),
max_length=50,
blank=True,
help_text=_("Local facility ID or description")
help_text=_('Local facility ID or description')
)
asns = models.ManyToManyField(
to='ipam.ASN',
@ -191,28 +195,32 @@ class Site(PrimaryModel):
blank=True
)
physical_address = models.CharField(
verbose_name=_('physical address'),
max_length=200,
blank=True,
help_text=_("Physical location of the building")
help_text=_('Physical location of the building')
)
shipping_address = models.CharField(
verbose_name=_('shipping address'),
max_length=200,
blank=True,
help_text=_("If different from the physical address")
help_text=_('If different from the physical address')
)
latitude = models.DecimalField(
verbose_name=_('latitude'),
max_digits=8,
decimal_places=6,
blank=True,
null=True,
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
)
longitude = models.DecimalField(
verbose_name=_('longitude'),
max_digits=9,
decimal_places=6,
blank=True,
null=True,
help_text=_("GPS coordinate in decimal format (xx.yyyyyy)")
help_text=_('GPS coordinate in decimal format (xx.yyyyyy)')
)
# Generic relations
@ -262,6 +270,7 @@ class Location(NestedGroupModel):
related_name='locations'
)
status = models.CharField(
verbose_name=_('status'),
max_length=50,
choices=LocationStatusChoices,
default=LocationStatusChoices.STATUS_ACTIVE
@ -304,7 +313,7 @@ class Location(NestedGroupModel):
fields=('site', 'name'),
name='%(app_label)s_%(class)s_name',
condition=Q(parent__isnull=True),
violation_error_message="A location with this name already exists within the specified site."
violation_error_message=_("A location with this name already exists within the specified site.")
),
models.UniqueConstraint(
fields=('site', 'parent', 'slug'),
@ -314,7 +323,7 @@ class Location(NestedGroupModel):
fields=('site', 'slug'),
name='%(app_label)s_%(class)s_slug',
condition=Q(parent__isnull=True),
violation_error_message="A location with this slug already exists within the specified site."
violation_error_message=_("A location with this slug already exists within the specified site.")
),
)
@ -329,4 +338,6 @@ class Location(NestedGroupModel):
# Parent Location (if any) must belong to the same Site
if self.parent and self.parent.site != self.site:
raise ValidationError(f"Parent location ({self.parent}) must belong to the same site ({self.site})")
raise ValidationError(_(
"Parent location ({parent}) must belong to the same site ({site})."
).format(parent=self.parent, site=self.site))

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django_tables2.utils import Accessor
from django.utils.safestring import mark_safe
@ -50,60 +51,60 @@ class CableTable(TenancyColumnsMixin, NetBoxTable):
a_terminations = CableTerminationsColumn(
cable_end='A',
orderable=False,
verbose_name='Termination A'
verbose_name=_('Termination A')
)
b_terminations = CableTerminationsColumn(
cable_end='B',
orderable=False,
verbose_name='Termination B'
verbose_name=_('Termination B')
)
device_a = CableTerminationsColumn(
cable_end='A',
attr='_device',
orderable=False,
verbose_name='Device A'
verbose_name=_('Device A')
)
device_b = CableTerminationsColumn(
cable_end='B',
attr='_device',
orderable=False,
verbose_name='Device B'
verbose_name=_('Device B')
)
location_a = CableTerminationsColumn(
cable_end='A',
attr='_location',
orderable=False,
verbose_name='Location A'
verbose_name=_('Location A')
)
location_b = CableTerminationsColumn(
cable_end='B',
attr='_location',
orderable=False,
verbose_name='Location B'
verbose_name=_('Location B')
)
rack_a = CableTerminationsColumn(
cable_end='A',
attr='_rack',
orderable=False,
verbose_name='Rack A'
verbose_name=_('Rack A')
)
rack_b = CableTerminationsColumn(
cable_end='B',
attr='_rack',
orderable=False,
verbose_name='Rack B'
verbose_name=_('Rack B')
)
site_a = CableTerminationsColumn(
cable_end='A',
attr='_site',
orderable=False,
verbose_name='Site A'
verbose_name=_('Site A')
)
site_b = CableTerminationsColumn(
cable_end='B',
attr='_site',
orderable=False,
verbose_name='Site B'
verbose_name=_('Site B')
)
status = columns.ChoiceFieldColumn()
length = columns.TemplateColumn(

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django_tables2.utils import Accessor
@ -18,15 +19,16 @@ __all__ = (
class ConsoleConnectionTable(PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify=True
)
name = tables.Column(
linkify=True,
verbose_name='Console Port'
verbose_name=_('Console Port')
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
verbose_name=_('Reachable')
)
class Meta(BaseTable.Meta):
@ -36,15 +38,16 @@ class ConsoleConnectionTable(PathEndpointTable):
class PowerConnectionTable(PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify=True
)
name = tables.Column(
linkify=True,
verbose_name='Power Port'
verbose_name=_('Power Port')
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
verbose_name=_('Reachable')
)
class Meta(BaseTable.Meta):
@ -54,16 +57,18 @@ class PowerConnectionTable(PathEndpointTable):
class InterfaceConnectionTable(PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
accessor=Accessor('device'),
linkify=True
)
interface = tables.Column(
verbose_name=_('Interface'),
accessor=Accessor('name'),
linkify=True
)
reachable = columns.BooleanColumn(
accessor=Accessor('_path__is_active'),
verbose_name='Reachable'
verbose_name=_('Reachable')
)
class Meta(BaseTable.Meta):

View File

@ -1,10 +1,11 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from dcim import models
from django_tables2.utils import Accessor
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from django.utils.translation import gettext as _
from dcim import models
from netbox.tables import NetBoxTable, columns
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from .template_code import *
__all__ = (
@ -76,17 +77,18 @@ def get_interface_state_attribute(record):
class DeviceRoleTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
device_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'role_id': 'pk'},
verbose_name='Devices'
verbose_name=_('Devices')
)
vm_count = columns.LinkedCountColumn(
viewname='virtualization:virtualmachine_list',
url_params={'role_id': 'pk'},
verbose_name='VMs'
verbose_name=_('VMs')
)
color = columns.ColorColumn()
vm_role = columns.BooleanColumn()
@ -112,23 +114,26 @@ class DeviceRoleTable(NetBoxTable):
class PlatformTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
manufacturer = tables.Column(
verbose_name=_('Manufacturer'),
linkify=True
)
config_template = tables.Column(
verbose_name=_('Config Template'),
linkify=True
)
device_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'platform_id': 'pk'},
verbose_name='Devices'
verbose_name=_('Devices')
)
vm_count = columns.LinkedCountColumn(
viewname='virtualization:virtualmachine_list',
url_params={'platform_id': 'pk'},
verbose_name='VMs'
verbose_name=_('VMs')
)
tags = columns.TagColumn(
url_name='dcim:platform_list'
@ -151,78 +156,94 @@ class PlatformTable(NetBoxTable):
class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
order_by=('_name',),
template_code=DEVICE_LINK,
linkify=True
)
status = columns.ChoiceFieldColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
region = tables.Column(
verbose_name=_('Region'),
accessor=Accessor('site__region'),
linkify=True
)
site_group = tables.Column(
accessor=Accessor('site__group'),
linkify=True,
verbose_name='Site Group'
verbose_name=_('Site Group')
)
site = tables.Column(
verbose_name=_('Site'),
linkify=True
)
location = tables.Column(
verbose_name=_('Location'),
linkify=True
)
rack = tables.Column(
verbose_name=_('Rack'),
linkify=True
)
position = columns.TemplateColumn(
verbose_name=_('Position'),
template_code='{{ value|floatformat }}'
)
device_role = columns.ColoredLabelColumn(
verbose_name='Role'
verbose_name=_('Role')
)
manufacturer = tables.Column(
verbose_name=_('Manufacturer'),
accessor=Accessor('device_type__manufacturer'),
linkify=True
)
device_type = tables.Column(
linkify=True,
verbose_name='Type'
verbose_name=_('Type')
)
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
verbose_name=_('IP Address')
)
primary_ip4 = tables.Column(
linkify=True,
verbose_name='IPv4 Address'
verbose_name=_('IPv4 Address')
)
primary_ip6 = tables.Column(
linkify=True,
verbose_name='IPv6 Address'
verbose_name=_('IPv6 Address')
)
oob_ip = tables.Column(
linkify=True,
verbose_name='OOB IP'
)
cluster = tables.Column(
verbose_name=_('Cluster'),
linkify=True
)
virtual_chassis = tables.Column(
verbose_name=_('Virtual Chassis'),
linkify=True
)
vc_position = tables.Column(
verbose_name='VC Position'
verbose_name=_('VC Position')
)
vc_priority = tables.Column(
verbose_name='VC Priority'
verbose_name=_('VC Priority')
)
config_template = tables.Column(
verbose_name=_('Config Template'),
linkify=True
)
parent_device = tables.Column(
verbose_name='Parent Device',
verbose_name=_('Parent Device'),
linkify=True,
accessor='parent_bay__device'
)
device_bay_position = tables.Column(
verbose_name='Position (Device Bay)',
verbose_name=_('Position (Device Bay)'),
accessor='parent_bay',
linkify=True
)
@ -230,6 +251,36 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
tags = columns.TagColumn(
url_name='dcim:device_list'
)
console_port_count = tables.Column(
verbose_name=_('Console ports')
)
console_server_port_count = tables.Column(
verbose_name=_('Console server ports')
)
power_port_count = tables.Column(
verbose_name=_('Power ports')
)
power_outlet_count = tables.Column(
verbose_name=_('Power outlets')
)
interface_count = tables.Column(
verbose_name=_('Interfaces')
)
front_port_count = tables.Column(
verbose_name=_('Front ports')
)
rear_port_count = tables.Column(
verbose_name=_('Rear ports')
)
device_bay_count = tables.Column(
verbose_name=_('Device bays')
)
module_bay_count = tables.Column(
verbose_name=_('Module bays')
)
inventory_item_count = tables.Column(
verbose_name=_('Inventory items')
)
class Meta(NetBoxTable.Meta):
model = models.Device
@ -237,8 +288,8 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
'pk', 'id', 'name', 'status', 'tenant', 'tenant_group', 'device_role', 'manufacturer', 'device_type',
'platform', 'serial', 'asset_tag', 'region', 'site_group', 'site', 'location', 'rack', 'parent_device',
'device_bay_position', 'position', 'face', 'latitude', 'longitude', 'airflow', 'primary_ip', 'primary_ip4',
'primary_ip6', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description', 'config_template',
'comments', 'contacts', 'tags', 'created', 'last_updated',
'primary_ip6', 'oob_ip', 'cluster', 'virtual_chassis', 'vc_position', 'vc_priority', 'description',
'config_template', 'comments', 'contacts', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'status', 'tenant', 'site', 'location', 'rack', 'device_role', 'manufacturer', 'device_type',
@ -248,21 +299,26 @@ class DeviceTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code=DEVICE_LINK,
linkify=True
)
status = columns.ChoiceFieldColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
site = tables.Column(
verbose_name=_('Site'),
linkify=True
)
rack = tables.Column(
verbose_name=_('Rack'),
linkify=True
)
device_role = tables.Column(
verbose_name='Role'
verbose_name=_('Role')
)
device_type = tables.Column(
verbose_name='Type'
verbose_name=_('Type')
)
class Meta(NetBoxTable.Meta):
@ -277,9 +333,11 @@ class DeviceImportTable(TenancyColumnsMixin, NetBoxTable):
class DeviceComponentTable(NetBoxTable):
device = tables.Column(
verbose_name=_('Device'),
linkify=True
)
name = tables.Column(
verbose_name=_('Name'),
linkify=True,
order_by=('_name',)
)
@ -290,6 +348,7 @@ class DeviceComponentTable(NetBoxTable):
class ModularDeviceComponentTable(DeviceComponentTable):
module_bay = tables.Column(
verbose_name=_('Module Bay'),
accessor=Accessor('module__module_bay'),
linkify={
'viewname': 'dcim:device_modulebays',
@ -297,39 +356,44 @@ class ModularDeviceComponentTable(DeviceComponentTable):
}
)
module = tables.Column(
verbose_name=_('Module'),
linkify=True
)
class CableTerminationTable(NetBoxTable):
cable = tables.Column(
verbose_name=_('Cable'),
linkify=True
)
cable_color = columns.ColorColumn(
accessor='cable__color',
orderable=False,
verbose_name='Cable Color'
verbose_name=_('Cable Color')
)
link_peer = columns.TemplateColumn(
accessor='link_peers',
template_code=LINKTERMINATION,
orderable=False,
verbose_name='Link Peers'
verbose_name=_('Link Peers')
)
mark_connected = columns.BooleanColumn(
verbose_name=_('Mark Connected'),
)
mark_connected = columns.BooleanColumn()
class PathEndpointTable(CableTerminationTable):
connection = columns.TemplateColumn(
accessor='_path__destinations',
template_code=LINKTERMINATION,
verbose_name='Connection',
verbose_name=_('Connection'),
orderable=False
)
class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_consoleports',
'args': [Accessor('device_id')],
@ -350,6 +414,7 @@ class ConsolePortTable(ModularDeviceComponentTable, PathEndpointTable):
class DeviceConsolePortTable(ConsolePortTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-console"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
@ -372,6 +437,7 @@ class DeviceConsolePortTable(ConsolePortTable):
class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_consoleserverports',
'args': [Accessor('device_id')],
@ -392,6 +458,7 @@ class ConsoleServerPortTable(ModularDeviceComponentTable, PathEndpointTable):
class DeviceConsoleServerPortTable(ConsoleServerPortTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-console-network-outline"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
@ -415,6 +482,7 @@ class DeviceConsoleServerPortTable(ConsoleServerPortTable):
class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_powerports',
'args': [Accessor('device_id')],
@ -436,6 +504,7 @@ class PowerPortTable(ModularDeviceComponentTable, PathEndpointTable):
class DevicePowerPortTable(PowerPortTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-power-plug-outline"></i> <a href="{{ record.get_absolute_url }}">'
'{{ value }}</a>',
order_by=Accessor('_name'),
@ -461,12 +530,14 @@ class DevicePowerPortTable(PowerPortTable):
class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_poweroutlets',
'args': [Accessor('device_id')],
}
)
power_port = tables.Column(
verbose_name=_('Power Port'),
linkify=True
)
tags = columns.TagColumn(
@ -485,6 +556,7 @@ class PowerOutletTable(ModularDeviceComponentTable, PathEndpointTable):
class DevicePowerOutletTable(PowerOutletTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-power-socket"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
attrs={'td': {'class': 'text-nowrap'}}
@ -508,29 +580,34 @@ class DevicePowerOutletTable(PowerOutletTable):
class BaseInterfaceTable(NetBoxTable):
enabled = columns.BooleanColumn()
enabled = columns.BooleanColumn(
verbose_name=_('Enabled'),
)
ip_addresses = tables.TemplateColumn(
template_code=INTERFACE_IPADDRESSES,
orderable=False,
verbose_name='IP Addresses'
verbose_name=_('IP Addresses')
)
fhrp_groups = tables.TemplateColumn(
accessor=Accessor('fhrp_group_assignments'),
template_code=INTERFACE_FHRPGROUPS,
orderable=False,
verbose_name='FHRP Groups'
verbose_name=_('FHRP Groups')
)
l2vpn = tables.Column(
accessor=tables.A('l2vpn_termination__l2vpn'),
linkify=True,
orderable=False,
verbose_name='L2VPN'
verbose_name=_('L2VPN')
)
untagged_vlan = tables.Column(
verbose_name=_('Untagged VLAN'),
linkify=True
)
untagged_vlan = tables.Column(linkify=True)
tagged_vlans = columns.TemplateColumn(
template_code=INTERFACE_TAGGED_VLANS,
orderable=False,
verbose_name='Tagged VLANs'
verbose_name=_('Tagged VLANs')
)
def value_ip_addresses(self, value):
@ -539,25 +616,30 @@ class BaseInterfaceTable(NetBoxTable):
class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpointTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_interfaces',
'args': [Accessor('device_id')],
}
)
mgmt_only = columns.BooleanColumn()
mgmt_only = columns.BooleanColumn(
verbose_name=_('Management Only'),
)
wireless_link = tables.Column(
verbose_name=_('Wireless link'),
linkify=True
)
wireless_lans = columns.TemplateColumn(
template_code=INTERFACE_WIRELESS_LANS,
orderable=False,
verbose_name='Wireless LANs'
verbose_name=_('Wireless LANs')
)
vdcs = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='VDCs'
verbose_name=_('VDCs')
)
vrf = tables.Column(
verbose_name=_('VRF'),
linkify=True
)
tags = columns.TagColumn(
@ -578,6 +660,7 @@ class InterfaceTable(ModularDeviceComponentTable, BaseInterfaceTable, PathEndpoi
class DeviceInterfaceTable(InterfaceTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-{% if record.mgmt_only %}wrench{% elif record.is_lag %}reorder-horizontal'
'{% elif record.is_virtual %}circle{% elif record.is_wireless %}wifi{% else %}ethernet'
'{% endif %}"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
@ -585,14 +668,16 @@ class DeviceInterfaceTable(InterfaceTable):
attrs={'td': {'class': 'text-nowrap'}}
)
parent = tables.Column(
verbose_name=_('Parent'),
linkify=True
)
bridge = tables.Column(
verbose_name=_('Bridge'),
linkify=True
)
lag = tables.Column(
linkify=True,
verbose_name='LAG'
verbose_name=_('LAG')
)
actions = columns.ActionsColumn(
extra_buttons=INTERFACE_BUTTONS
@ -621,16 +706,20 @@ class DeviceInterfaceTable(InterfaceTable):
class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_frontports',
'args': [Accessor('device_id')],
}
)
color = columns.ColorColumn()
color = columns.ColorColumn(
verbose_name=_('Color'),
)
rear_port_position = tables.Column(
verbose_name='Position'
verbose_name=_('Position')
)
rear_port = tables.Column(
verbose_name=_('Rear Port'),
linkify=True
)
tags = columns.TagColumn(
@ -651,6 +740,7 @@ class FrontPortTable(ModularDeviceComponentTable, CableTerminationTable):
class DeviceFrontPortTable(FrontPortTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
@ -676,12 +766,15 @@ class DeviceFrontPortTable(FrontPortTable):
class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_rearports',
'args': [Accessor('device_id')],
}
)
color = columns.ColorColumn()
color = columns.ColorColumn(
verbose_name=_('Color'),
)
tags = columns.TagColumn(
url_name='dcim:rearport_list'
)
@ -697,6 +790,7 @@ class RearPortTable(ModularDeviceComponentTable, CableTerminationTable):
class DeviceRearPortTable(RearPortTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-square-rounded{% if not record.cable %}-outline{% endif %}"></i> '
'<a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
@ -722,6 +816,7 @@ class DeviceRearPortTable(RearPortTable):
class DeviceBayTable(DeviceComponentTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_devicebays',
'args': [Accessor('device_id')],
@ -729,18 +824,20 @@ class DeviceBayTable(DeviceComponentTable):
)
device_role = columns.ColoredLabelColumn(
accessor=Accessor('installed_device__device_role'),
verbose_name='Role'
verbose_name=_('Role')
)
device_type = tables.Column(
accessor=Accessor('installed_device__device_type'),
linkify=True,
verbose_name='Type'
verbose_name=_('Type')
)
status = tables.TemplateColumn(
verbose_name=_('Status'),
template_code=DEVICEBAY_STATUS,
order_by=Accessor('installed_device__status')
)
installed_device = tables.Column(
verbose_name=_('Installed device'),
linkify=True
)
tags = columns.TagColumn(
@ -759,6 +856,7 @@ class DeviceBayTable(DeviceComponentTable):
class DeviceDeviceBayTable(DeviceBayTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<i class="mdi mdi-circle{% if record.installed_device %}slice-8{% else %}outline{% endif %}'
'"></i> <a href="{{ record.get_absolute_url }}">{{ value }}</a>',
order_by=Accessor('_name'),
@ -778,6 +876,7 @@ class DeviceDeviceBayTable(DeviceBayTable):
class ModuleBayTable(DeviceComponentTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_modulebays',
'args': [Accessor('device_id')],
@ -785,18 +884,21 @@ class ModuleBayTable(DeviceComponentTable):
)
installed_module = tables.Column(
linkify=True,
verbose_name='Installed module'
verbose_name=_('Installed Module')
)
module_serial = tables.Column(
verbose_name=_('Module Serial'),
accessor=tables.A('installed_module__serial')
)
module_asset_tag = tables.Column(
verbose_name=_('Module Asset Tag'),
accessor=tables.A('installed_module__asset_tag')
)
tags = columns.TagColumn(
url_name='dcim:modulebay_list'
)
module_status = columns.TemplateColumn(
verbose_name=_('Module Status'),
template_code=MODULEBAY_STATUS
)
@ -825,20 +927,27 @@ class DeviceModuleBayTable(ModuleBayTable):
class InventoryItemTable(DeviceComponentTable):
device = tables.Column(
verbose_name=_('Device'),
linkify={
'viewname': 'dcim:device_inventory',
'args': [Accessor('device_id')],
}
)
role = columns.ColoredLabelColumn()
role = columns.ColoredLabelColumn(
verbose_name=_('Role'),
)
manufacturer = tables.Column(
verbose_name=_('Manufacturer'),
linkify=True
)
component = tables.Column(
verbose_name=_('Component'),
orderable=False,
linkify=True
)
discovered = columns.BooleanColumn()
discovered = columns.BooleanColumn(
verbose_name=_('Discovered'),
)
tags = columns.TagColumn(
url_name='dcim:inventoryitem_list'
)
@ -857,6 +966,7 @@ class InventoryItemTable(DeviceComponentTable):
class DeviceInventoryItemTable(InventoryItemTable):
name = tables.TemplateColumn(
verbose_name=_('Name'),
template_code='<a href="{{ record.get_absolute_url }}" style="padding-left: {{ record.level }}0px">'
'{{ value }}</a>',
order_by=Accessor('_name'),
@ -876,14 +986,17 @@ class DeviceInventoryItemTable(InventoryItemTable):
class InventoryItemRoleTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
inventoryitem_count = columns.LinkedCountColumn(
viewname='dcim:inventoryitem_list',
url_params={'role_id': 'pk'},
verbose_name='Items'
verbose_name=_('Items')
)
color = columns.ColorColumn(
verbose_name=_('Color'),
)
color = columns.ColorColumn()
tags = columns.TagColumn(
url_name='dcim:inventoryitemrole_list'
)
@ -902,17 +1015,21 @@ class InventoryItemRoleTable(NetBoxTable):
class VirtualChassisTable(NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
master = tables.Column(
verbose_name=_('Master'),
linkify=True
)
member_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'virtual_chassis_id': 'pk'},
verbose_name='Members'
verbose_name=_('Members')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:virtualchassis_list'
)
@ -928,31 +1045,35 @@ class VirtualChassisTable(NetBoxTable):
class VirtualDeviceContextTable(TenancyColumnsMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
device = tables.TemplateColumn(
verbose_name=_('Device'),
order_by=('_name',),
template_code=DEVICE_LINK,
linkify=True
)
status = columns.ChoiceFieldColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
primary_ip = tables.Column(
linkify=True,
order_by=('primary_ip4', 'primary_ip6'),
verbose_name='IP Address'
verbose_name=_('IP Address')
)
primary_ip4 = tables.Column(
linkify=True,
verbose_name='IPv4 Address'
verbose_name=_('IPv4 Address')
)
primary_ip6 = tables.Column(
linkify=True,
verbose_name='IPv6 Address'
verbose_name=_('IPv6 Address')
)
interface_count = columns.LinkedCountColumn(
viewname='dcim:interface_list',
url_params={'vdc_id': 'pk'},
verbose_name='Interfaces'
verbose_name=_('Interfaces')
)
comments = columns.MarkdownColumn()

View File

@ -1,4 +1,6 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django.utils.translation import gettext as _
from dcim import models
from netbox.tables import NetBoxTable, columns
@ -27,27 +29,28 @@ __all__ = (
class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
devicetype_count = columns.LinkedCountColumn(
viewname='dcim:devicetype_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Device Types'
verbose_name=_('Device Types')
)
moduletype_count = columns.LinkedCountColumn(
viewname='dcim:moduletype_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Module Types'
verbose_name=_('Module Types')
)
inventoryitem_count = columns.LinkedCountColumn(
viewname='dcim:inventoryitem_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Inventory Items'
verbose_name=_('Inventory Items')
)
platform_count = columns.LinkedCountColumn(
viewname='dcim:platform_list',
url_params={'manufacturer_id': 'pk'},
verbose_name='Platforms'
verbose_name=_('Platforms')
)
tags = columns.TagColumn(
url_name='dcim:manufacturer_list'
@ -72,39 +75,76 @@ class ManufacturerTable(ContactsColumnMixin, NetBoxTable):
class DeviceTypeTable(NetBoxTable):
model = tables.Column(
linkify=True,
verbose_name='Device Type'
verbose_name=_('Device Type')
)
manufacturer = tables.Column(
verbose_name=_('Manufacturer'),
linkify=True
)
default_platform = tables.Column(
verbose_name=_('Default Platform'),
linkify=True
)
is_full_depth = columns.BooleanColumn(
verbose_name='Full Depth'
verbose_name=_('Full Depth')
)
instance_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'device_type_id': 'pk'},
verbose_name='Instances'
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:devicetype_list'
)
u_height = columns.TemplateColumn(
verbose_name=_('U Height'),
template_code='{{ value|floatformat }}'
)
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)
instance_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'device_type_id': 'pk'},
verbose_name=_('Instances')
)
console_port_template_count = tables.Column(
verbose_name=_('Console Ports')
)
console_server_port_template_count = tables.Column(
verbose_name=_('Console Server Ports')
)
power_port_template_count = tables.Column(
verbose_name=_('Power Ports')
)
power_outlet_template_count = tables.Column(
verbose_name=_('Power Outlets')
)
interface_template_count = tables.Column(
verbose_name=_('Interfaces')
)
front_port_template_count = tables.Column(
verbose_name=_('Front Ports')
)
rear_port_template_count = tables.Column(
verbose_name=_('Rear Ports')
)
device_bay_template_count = tables.Column(
verbose_name=_('Device Bays')
)
module_bay_template_count = tables.Column(
verbose_name=_('Module Bays')
)
inventory_item_template_count = tables.Column(
verbose_name=_('Inventory Items')
)
class Meta(NetBoxTable.Meta):
model = models.DeviceType
fields = (
'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created', 'last_updated',
'pk', 'id', 'model', 'manufacturer', 'default_platform', 'slug', 'part_number', 'u_height', 'is_full_depth',
'subdevice_role', 'airflow', 'weight', 'description', 'comments', 'instance_count', 'tags', 'created',
'last_updated',
)
default_columns = (
'pk', 'model', 'manufacturer', 'part_number', 'u_height', 'is_full_depth', 'instance_count',
@ -117,7 +157,7 @@ class DeviceTypeTable(NetBoxTable):
class ComponentTemplateTable(NetBoxTable):
id = tables.Column(
verbose_name='ID'
verbose_name=_('ID')
)
name = tables.Column(
order_by=('_name',)
@ -176,9 +216,11 @@ class PowerOutletTemplateTable(ComponentTemplateTable):
class InterfaceTemplateTable(ComponentTemplateTable):
enabled = columns.BooleanColumn()
enabled = columns.BooleanColumn(
verbose_name=_('Enabled'),
)
mgmt_only = columns.BooleanColumn(
verbose_name='Management Only'
verbose_name=_('Management Only')
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
@ -187,15 +229,20 @@ class InterfaceTemplateTable(ComponentTemplateTable):
class Meta(ComponentTemplateTable.Meta):
model = models.InterfaceTemplate
fields = ('pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'bridge', 'poe_mode', 'poe_type', 'actions')
fields = (
'pk', 'name', 'label', 'enabled', 'mgmt_only', 'type', 'description', 'bridge', 'poe_mode', 'poe_type',
'rf_role', 'actions',
)
empty_text = "None"
class FrontPortTemplateTable(ComponentTemplateTable):
rear_port_position = tables.Column(
verbose_name='Position'
verbose_name=_('Position')
)
color = columns.ColorColumn(
verbose_name=_('Color'),
)
color = columns.ColorColumn()
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
@ -208,7 +255,9 @@ class FrontPortTemplateTable(ComponentTemplateTable):
class RearPortTemplateTable(ComponentTemplateTable):
color = columns.ColorColumn()
color = columns.ColorColumn(
verbose_name=_('Color'),
)
actions = columns.ActionsColumn(
actions=('edit', 'delete'),
extra_buttons=MODULAR_COMPONENT_TEMPLATE_BUTTONS
@ -247,12 +296,15 @@ class InventoryItemTemplateTable(ComponentTemplateTable):
actions=('edit', 'delete')
)
role = tables.Column(
verbose_name=_('Role'),
linkify=True
)
manufacturer = tables.Column(
verbose_name=_('Manufacturer'),
linkify=True
)
component = tables.Column(
verbose_name=_('Component'),
orderable=False
)

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from dcim.models import Module, ModuleType
@ -13,21 +14,25 @@ __all__ = (
class ModuleTypeTable(NetBoxTable):
model = tables.Column(
linkify=True,
verbose_name='Module Type'
verbose_name=_('Module Type')
)
manufacturer = tables.Column(
verbose_name=_('Manufacturer'),
linkify=True
)
instance_count = columns.LinkedCountColumn(
viewname='dcim:module_list',
url_params={'module_type_id': 'pk'},
verbose_name='Instances'
verbose_name=_('Instances')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:moduletype_list'
)
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)
@ -44,20 +49,28 @@ class ModuleTypeTable(NetBoxTable):
class ModuleTable(NetBoxTable):
device = tables.Column(
verbose_name=_('Device'),
linkify=True
)
module_bay = tables.Column(
verbose_name=_('Module Bay'),
linkify=True
)
manufacturer = tables.Column(
verbose_name=_('Manufacturer'),
accessor=tables.A('module_type__manufacturer'),
linkify=True
)
module_type = tables.Column(
verbose_name=_('Module Type'),
linkify=True
)
status = columns.ChoiceFieldColumn()
comments = columns.MarkdownColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
tags = columns.TagColumn(
url_name='dcim:module_list'
)

View File

@ -1,6 +1,7 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from dcim.models import PowerFeed, PowerPanel
from tenancy.tables import ContactsColumnMixin
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
from netbox.tables import NetBoxTable, columns
@ -18,20 +19,25 @@ __all__ = (
class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
site = tables.Column(
verbose_name=_('Site'),
linkify=True
)
location = tables.Column(
verbose_name=_('Location'),
linkify=True
)
powerfeed_count = columns.LinkedCountColumn(
viewname='dcim:powerfeed_list',
url_params={'power_panel_id': 'pk'},
verbose_name='Feeds'
verbose_name=_('Power Feeds')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:powerpanel_list'
)
@ -51,25 +57,39 @@ class PowerPanelTable(ContactsColumnMixin, NetBoxTable):
# We're not using PathEndpointTable for PowerFeed because power connections
# cannot traverse pass-through ports.
class PowerFeedTable(CableTerminationTable):
class PowerFeedTable(TenancyColumnsMixin, CableTerminationTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
power_panel = tables.Column(
verbose_name=_('Power Panel'),
linkify=True
)
rack = tables.Column(
verbose_name=_('Rack'),
linkify=True
)
status = columns.ChoiceFieldColumn()
type = columns.ChoiceFieldColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
type = columns.ChoiceFieldColumn(
verbose_name=_('Type'),
)
max_utilization = tables.TemplateColumn(
verbose_name=_('Max Utilization'),
template_code="{{ value }}%"
)
available_power = tables.Column(
verbose_name='Available power (VA)'
verbose_name=_('Available Power (VA)')
)
tenant = tables.Column(
linkify=True,
verbose_name=_('Tenant')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:powerfeed_list'
)
@ -78,8 +98,8 @@ class PowerFeedTable(CableTerminationTable):
model = PowerFeed
fields = (
'pk', 'id', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power',
'description', 'comments', 'tags', 'created', 'last_updated',
'max_utilization', 'mark_connected', 'cable', 'cable_color', 'link_peer', 'available_power', 'tenant',
'tenant_group', 'description', 'comments', 'tags', 'created', 'last_updated',
)
default_columns = (
'pk', 'name', 'power_panel', 'rack', 'status', 'type', 'supply', 'voltage', 'amperage', 'phase', 'cable',

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from django_tables2.utils import Accessor
@ -18,13 +19,18 @@ __all__ = (
#
class RackRoleTable(NetBoxTable):
name = tables.Column(linkify=True)
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
rack_count = columns.LinkedCountColumn(
viewname='dcim:rack_list',
url_params={'role_id': 'pk'},
verbose_name='Racks'
verbose_name=_('Racks')
)
color = columns.ColorColumn(
verbose_name=_('Color'),
)
color = columns.ColorColumn()
tags = columns.TagColumn(
url_name='dcim:rackrole_list'
)
@ -44,51 +50,62 @@ class RackRoleTable(NetBoxTable):
class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
order_by=('_name',),
linkify=True
)
location = tables.Column(
verbose_name=_('Location'),
linkify=True
)
site = tables.Column(
verbose_name=_('Site'),
linkify=True
)
status = columns.ChoiceFieldColumn()
role = columns.ColoredLabelColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
role = columns.ColoredLabelColumn(
verbose_name=_('Role'),
)
u_height = tables.TemplateColumn(
template_code="{{ value }}U",
verbose_name='Height'
verbose_name=_('Height')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
device_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'rack_id': 'pk'},
verbose_name='Devices'
verbose_name=_('Devices')
)
get_utilization = columns.UtilizationColumn(
orderable=False,
verbose_name='Space'
verbose_name=_('Space')
)
get_power_utilization = columns.UtilizationColumn(
orderable=False,
verbose_name='Power'
verbose_name=_('Power')
)
tags = columns.TagColumn(
url_name='dcim:rack_list'
)
outer_width = tables.TemplateColumn(
template_code="{{ record.outer_width }} {{ record.outer_unit }}",
verbose_name='Outer Width'
verbose_name=_('Outer Width')
)
outer_depth = tables.TemplateColumn(
template_code="{{ record.outer_depth }} {{ record.outer_unit }}",
verbose_name='Outer Depth'
verbose_name=_('Outer Depth')
)
weight = columns.TemplateColumn(
verbose_name=_('Weight'),
template_code=WEIGHT,
order_by=('_abs_weight', 'weight_unit')
)
max_weight = columns.TemplateColumn(
verbose_name=_('Max Weight'),
template_code=WEIGHT,
order_by=('_abs_max_weight', 'weight_unit')
)
@ -113,25 +130,31 @@ class RackTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class RackReservationTable(TenancyColumnsMixin, NetBoxTable):
reservation = tables.Column(
verbose_name=_('Reservation'),
accessor='pk',
linkify=True
)
site = tables.Column(
verbose_name=_('Site'),
accessor=Accessor('rack__site'),
linkify=True
)
location = tables.Column(
verbose_name=_('Location'),
accessor=Accessor('rack__location'),
linkify=True
)
rack = tables.Column(
verbose_name=_('Rack'),
linkify=True
)
unit_list = tables.Column(
orderable=False,
verbose_name='Units'
verbose_name=_('Units')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:rackreservation_list'
)

View File

@ -1,3 +1,4 @@
from django.utils.translation import gettext_lazy as _
import django_tables2 as tables
from dcim.models import Location, Region, Site, SiteGroup
from tenancy.tables import ContactsColumnMixin, TenancyColumnsMixin
@ -20,12 +21,13 @@ __all__ = (
class RegionTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
verbose_name=_('Name'),
linkify=True
)
site_count = columns.LinkedCountColumn(
viewname='dcim:site_list',
url_params={'region_id': 'pk'},
verbose_name='Sites'
verbose_name=_('Sites')
)
tags = columns.TagColumn(
url_name='dcim:region_list'
@ -46,12 +48,13 @@ class RegionTable(ContactsColumnMixin, NetBoxTable):
class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
verbose_name=_('Name'),
linkify=True
)
site_count = columns.LinkedCountColumn(
viewname='dcim:site_list',
url_params={'group_id': 'pk'},
verbose_name='Sites'
verbose_name=_('Sites')
)
tags = columns.TagColumn(
url_name='dcim:sitegroup_list'
@ -72,26 +75,33 @@ class SiteGroupTable(ContactsColumnMixin, NetBoxTable):
class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = tables.Column(
verbose_name=_('Name'),
linkify=True
)
status = columns.ChoiceFieldColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
region = tables.Column(
verbose_name=_('Region'),
linkify=True
)
group = tables.Column(
verbose_name=_('Group'),
linkify=True
)
asns = columns.ManyToManyColumn(
linkify_item=True,
verbose_name='ASNs'
verbose_name=_('ASNs')
)
asn_count = columns.LinkedCountColumn(
accessor=tables.A('asns__count'),
viewname='ipam:asn_list',
url_params={'site_id': 'pk'},
verbose_name='ASN Count'
verbose_name=_('ASN Count')
)
comments = columns.MarkdownColumn(
verbose_name=_('Comments'),
)
comments = columns.MarkdownColumn()
tags = columns.TagColumn(
url_name='dcim:site_list'
)
@ -112,21 +122,25 @@ class SiteTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
class LocationTable(TenancyColumnsMixin, ContactsColumnMixin, NetBoxTable):
name = columns.MPTTColumn(
verbose_name=_('Name'),
linkify=True
)
site = tables.Column(
verbose_name=_('Site'),
linkify=True
)
status = columns.ChoiceFieldColumn()
status = columns.ChoiceFieldColumn(
verbose_name=_('Status'),
)
rack_count = columns.LinkedCountColumn(
viewname='dcim:rack_list',
url_params={'location_id': 'pk'},
verbose_name='Racks'
verbose_name=_('Racks')
)
device_count = columns.LinkedCountColumn(
viewname='dcim:device_list',
url_params={'location_id': 'pk'},
verbose_name='Devices'
verbose_name=_('Devices')
)
tags = columns.TagColumn(
url_name='dcim:location_list'

View File

@ -4419,6 +4419,21 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
)
Rack.objects.bulk_create(racks)
tenant_groups = (
TenantGroup(name='Tenant group 1', slug='tenant-group-1'),
TenantGroup(name='Tenant group 2', slug='tenant-group-2'),
TenantGroup(name='Tenant group 3', slug='tenant-group-3'),
)
for tenantgroup in tenant_groups:
tenantgroup.save()
tenants = (
Tenant(name='Tenant 1', slug='tenant-1', group=tenant_groups[0]),
Tenant(name='Tenant 2', slug='tenant-2', group=tenant_groups[1]),
Tenant(name='Tenant 3', slug='tenant-3', group=tenant_groups[2]),
)
Tenant.objects.bulk_create(tenants)
power_panels = (
PowerPanel(name='Power Panel 1', site=sites[0]),
PowerPanel(name='Power Panel 2', site=sites[1]),
@ -4427,9 +4442,44 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
PowerPanel.objects.bulk_create(power_panels)
power_feeds = (
PowerFeed(power_panel=power_panels[0], rack=racks[0], name='Power Feed 1', status=PowerFeedStatusChoices.STATUS_ACTIVE, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=100, amperage=100, max_utilization=10),
PowerFeed(power_panel=power_panels[1], rack=racks[1], name='Power Feed 2', status=PowerFeedStatusChoices.STATUS_FAILED, type=PowerFeedTypeChoices.TYPE_PRIMARY, supply=PowerFeedSupplyChoices.SUPPLY_AC, phase=PowerFeedPhaseChoices.PHASE_3PHASE, voltage=200, amperage=200, max_utilization=20),
PowerFeed(power_panel=power_panels[2], rack=racks[2], name='Power Feed 3', status=PowerFeedStatusChoices.STATUS_OFFLINE, type=PowerFeedTypeChoices.TYPE_REDUNDANT, supply=PowerFeedSupplyChoices.SUPPLY_DC, phase=PowerFeedPhaseChoices.PHASE_SINGLE, voltage=300, amperage=300, max_utilization=30),
PowerFeed(
power_panel=power_panels[0],
rack=racks[0],
name='Power Feed 1',
tenant=tenants[0],
status=PowerFeedStatusChoices.STATUS_ACTIVE,
type=PowerFeedTypeChoices.TYPE_PRIMARY,
supply=PowerFeedSupplyChoices.SUPPLY_AC,
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=100,
amperage=100,
max_utilization=10
),
PowerFeed(
power_panel=power_panels[1],
rack=racks[1],
name='Power Feed 2',
tenant=tenants[1],
status=PowerFeedStatusChoices.STATUS_FAILED,
type=PowerFeedTypeChoices.TYPE_PRIMARY,
supply=PowerFeedSupplyChoices.SUPPLY_AC,
phase=PowerFeedPhaseChoices.PHASE_3PHASE,
voltage=200,
amperage=200,
max_utilization=20),
PowerFeed(
power_panel=power_panels[2],
rack=racks[2],
name='Power Feed 3',
tenant=tenants[2],
status=PowerFeedStatusChoices.STATUS_OFFLINE,
type=PowerFeedTypeChoices.TYPE_REDUNDANT,
supply=PowerFeedSupplyChoices.SUPPLY_DC,
phase=PowerFeedPhaseChoices.PHASE_SINGLE,
voltage=300,
amperage=300,
max_utilization=30
),
)
PowerFeed.objects.bulk_create(power_feeds)
@ -4520,6 +4570,20 @@ class PowerFeedTestCase(TestCase, ChangeLoggedFilterSetTests):
params = {'connected': False}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
def test_tenant(self):
tenants = Tenant.objects.all()[:2]
params = {'tenant_id': [tenants[0].pk, tenants[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant': [tenants[0].slug, tenants[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
def test_tenant_group(self):
tenant_groups = TenantGroup.objects.all()[:2]
params = {'tenant_group_id': [tenant_groups[0].pk, tenant_groups[1].pk]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
params = {'tenant_group': [tenant_groups[0].slug, tenant_groups[1].slug]}
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
class VirtualDeviceContextTestCase(TestCase, ChangeLoggedFilterSetTests):
queryset = VirtualDeviceContext.objects.all()

View File

@ -681,13 +681,6 @@ class RackView(generic.ObjectView):
(PowerFeed.objects.restrict(request.user).filter(rack=instance), 'rack_id'),
)
# Get 0U devices located within the rack
nonracked_devices = Device.objects.filter(
rack=instance,
position__isnull=True,
parent_bay__isnull=True
).prefetch_related('device_type__manufacturer', 'parent_bay', 'device_role')
peer_racks = Rack.objects.restrict(request.user, 'view').filter(site=instance.site)
if instance.location:
@ -704,7 +697,6 @@ class RackView(generic.ObjectView):
return {
'related_models': related_models,
'nonracked_devices': nonracked_devices,
'next_rack': next_rack,
'prev_rack': prev_rack,
'svg_extra': svg_extra,
@ -731,6 +723,26 @@ class RackRackReservationsView(generic.ObjectChildrenView):
return parent.reservations.restrict(request.user, 'view')
@register_model_view(Rack, 'nonracked_devices', 'nonracked-devices')
class RackNonRackedView(generic.ObjectChildrenView):
queryset = Rack.objects.all()
child_model = Device
table = tables.DeviceTable
filterset = filtersets.DeviceFilterSet
template_name = 'dcim/rack/non_racked_devices.html'
tab = ViewTab(
label=_('Non-Racked Devices'),
badge=lambda obj: obj.devices.filter(rack=obj, position__isnull=True, parent_bay__isnull=True).count(),
weight=500,
permission='dcim.view_device',
)
def get_children(self, request, parent):
return parent.devices.restrict(request.user, 'view').filter(
rack=parent, position__isnull=True, parent_bay__isnull=True
)
@register_model_view(Rack, 'edit')
class RackEditView(generic.ObjectEditView):
queryset = Rack.objects.all()
@ -951,7 +963,7 @@ class DeviceTypeConsolePortsView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_consoleports'
tab = ViewTab(
label=_('Console Ports'),
badge=lambda obj: obj.consoleporttemplates.count(),
badge=lambda obj: obj.console_port_template_count,
permission='dcim.view_consoleporttemplate',
weight=550,
hide_if_empty=True
@ -966,7 +978,7 @@ class DeviceTypeConsoleServerPortsView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_consoleserverports'
tab = ViewTab(
label=_('Console Server Ports'),
badge=lambda obj: obj.consoleserverporttemplates.count(),
badge=lambda obj: obj.console_server_port_template_count,
permission='dcim.view_consoleserverporttemplate',
weight=560,
hide_if_empty=True
@ -981,7 +993,7 @@ class DeviceTypePowerPortsView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_powerports'
tab = ViewTab(
label=_('Power Ports'),
badge=lambda obj: obj.powerporttemplates.count(),
badge=lambda obj: obj.power_port_template_count,
permission='dcim.view_powerporttemplate',
weight=570,
hide_if_empty=True
@ -996,7 +1008,7 @@ class DeviceTypePowerOutletsView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_poweroutlets'
tab = ViewTab(
label=_('Power Outlets'),
badge=lambda obj: obj.poweroutlettemplates.count(),
badge=lambda obj: obj.power_outlet_template_count,
permission='dcim.view_poweroutlettemplate',
weight=580,
hide_if_empty=True
@ -1011,7 +1023,7 @@ class DeviceTypeInterfacesView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_interfaces'
tab = ViewTab(
label=_('Interfaces'),
badge=lambda obj: obj.interfacetemplates.count(),
badge=lambda obj: obj.interface_template_count,
permission='dcim.view_interfacetemplate',
weight=520,
hide_if_empty=True
@ -1026,7 +1038,7 @@ class DeviceTypeFrontPortsView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_frontports'
tab = ViewTab(
label=_('Front Ports'),
badge=lambda obj: obj.frontporttemplates.count(),
badge=lambda obj: obj.front_port_template_count,
permission='dcim.view_frontporttemplate',
weight=530,
hide_if_empty=True
@ -1041,7 +1053,7 @@ class DeviceTypeRearPortsView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_rearports'
tab = ViewTab(
label=_('Rear Ports'),
badge=lambda obj: obj.rearporttemplates.count(),
badge=lambda obj: obj.rear_port_template_count,
permission='dcim.view_rearporttemplate',
weight=540,
hide_if_empty=True
@ -1056,7 +1068,7 @@ class DeviceTypeModuleBaysView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_modulebays'
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.modulebaytemplates.count(),
badge=lambda obj: obj.module_bay_template_count,
permission='dcim.view_modulebaytemplate',
weight=510,
hide_if_empty=True
@ -1071,7 +1083,7 @@ class DeviceTypeDeviceBaysView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_devicebays'
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.devicebaytemplates.count(),
badge=lambda obj: obj.device_bay_template_count,
permission='dcim.view_devicebaytemplate',
weight=500,
hide_if_empty=True
@ -1086,7 +1098,7 @@ class DeviceTypeInventoryItemsView(DeviceTypeComponentsView):
viewname = 'dcim:devicetype_inventoryitems'
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventoryitemtemplates.count(),
badge=lambda obj: obj.inventory_item_template_count,
permission='dcim.view_invenotryitemtemplate',
weight=590,
hide_if_empty=True
@ -1876,7 +1888,7 @@ class DeviceConsolePortsView(DeviceComponentsView):
template_name = 'dcim/device/consoleports.html',
tab = ViewTab(
label=_('Console Ports'),
badge=lambda obj: obj.consoleports.count(),
badge=lambda obj: obj.console_port_count,
permission='dcim.view_consoleport',
weight=550,
hide_if_empty=True
@ -1891,7 +1903,7 @@ class DeviceConsoleServerPortsView(DeviceComponentsView):
template_name = 'dcim/device/consoleserverports.html'
tab = ViewTab(
label=_('Console Server Ports'),
badge=lambda obj: obj.consoleserverports.count(),
badge=lambda obj: obj.console_server_port_count,
permission='dcim.view_consoleserverport',
weight=560,
hide_if_empty=True
@ -1906,7 +1918,7 @@ class DevicePowerPortsView(DeviceComponentsView):
template_name = 'dcim/device/powerports.html'
tab = ViewTab(
label=_('Power Ports'),
badge=lambda obj: obj.powerports.count(),
badge=lambda obj: obj.power_port_count,
permission='dcim.view_powerport',
weight=570,
hide_if_empty=True
@ -1921,7 +1933,7 @@ class DevicePowerOutletsView(DeviceComponentsView):
template_name = 'dcim/device/poweroutlets.html'
tab = ViewTab(
label=_('Power Outlets'),
badge=lambda obj: obj.poweroutlets.count(),
badge=lambda obj: obj.power_outlet_count,
permission='dcim.view_poweroutlet',
weight=580,
hide_if_empty=True
@ -1957,7 +1969,7 @@ class DeviceFrontPortsView(DeviceComponentsView):
template_name = 'dcim/device/frontports.html'
tab = ViewTab(
label=_('Front Ports'),
badge=lambda obj: obj.frontports.count(),
badge=lambda obj: obj.front_port_count,
permission='dcim.view_frontport',
weight=530,
hide_if_empty=True
@ -1972,7 +1984,7 @@ class DeviceRearPortsView(DeviceComponentsView):
template_name = 'dcim/device/rearports.html'
tab = ViewTab(
label=_('Rear Ports'),
badge=lambda obj: obj.rearports.count(),
badge=lambda obj: obj.rear_port_count,
permission='dcim.view_rearport',
weight=540,
hide_if_empty=True
@ -1987,7 +1999,7 @@ class DeviceModuleBaysView(DeviceComponentsView):
template_name = 'dcim/device/modulebays.html'
tab = ViewTab(
label=_('Module Bays'),
badge=lambda obj: obj.modulebays.count(),
badge=lambda obj: obj.module_bay_count,
permission='dcim.view_modulebay',
weight=510,
hide_if_empty=True
@ -2002,7 +2014,7 @@ class DeviceDeviceBaysView(DeviceComponentsView):
template_name = 'dcim/device/devicebays.html'
tab = ViewTab(
label=_('Device Bays'),
badge=lambda obj: obj.devicebays.count(),
badge=lambda obj: obj.device_bay_count,
permission='dcim.view_devicebay',
weight=500,
hide_if_empty=True
@ -2017,7 +2029,7 @@ class DeviceInventoryView(DeviceComponentsView):
template_name = 'dcim/device/inventory.html'
tab = ViewTab(
label=_('Inventory Items'),
badge=lambda obj: obj.inventoryitems.count(),
badge=lambda obj: obj.inventory_item_count,
permission='dcim.view_inventoryitem',
weight=590,
hide_if_empty=True
@ -2452,11 +2464,13 @@ class InterfaceView(generic.ObjectView):
queryset = Interface.objects.all()
def get_extra_context(self, request, instance):
# Get assigned VDC's
# Get assigned VDCs
vdc_table = tables.VirtualDeviceContextTable(
data=instance.vdcs.restrict(request.user, 'view').prefetch_related('device'),
exclude=('tenant', 'tenant_group', 'primary_ip', 'primary_ip4', 'primary_ip6', 'comments', 'tags',
'created', 'last_updated', 'actions', ),
exclude=(
'tenant', 'tenant_group', 'primary_ip', 'primary_ip4', 'primary_ip6', 'oob_ip', 'comments', 'tags',
'created', 'last_updated', 'actions',
),
orderable=False
)
@ -3225,9 +3239,7 @@ class InterfaceConnectionsListView(generic.ObjectListView):
#
class VirtualChassisListView(generic.ObjectListView):
queryset = VirtualChassis.objects.annotate(
member_count=count_related(Device, 'virtual_chassis')
)
queryset = VirtualChassis.objects.all()
table = tables.VirtualChassisTable
filterset = filtersets.VirtualChassisFilterSet
filterset_form = forms.VirtualChassisFilterForm

View File

@ -7,6 +7,7 @@ __all__ = [
'NestedBookmarkSerializer',
'NestedConfigContextSerializer',
'NestedConfigTemplateSerializer',
'NestedCustomFieldChoiceSetSerializer',
'NestedCustomFieldSerializer',
'NestedCustomLinkSerializer',
'NestedExportTemplateSerializer',
@ -34,6 +35,14 @@ class NestedCustomFieldSerializer(WritableNestedSerializer):
fields = ['id', 'url', 'display', 'name']
class NestedCustomFieldChoiceSetSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
class Meta:
model = models.CustomFieldChoiceSet
fields = ['id', 'url', 'display', 'name', 'choices_count']
class NestedCustomLinkSerializer(WritableNestedSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customlink-detail')

View File

@ -35,6 +35,7 @@ __all__ = (
'ConfigContextSerializer',
'ConfigTemplateSerializer',
'ContentTypeSerializer',
'CustomFieldChoiceSetSerializer',
'CustomFieldSerializer',
'CustomLinkSerializer',
'DashboardSerializer',
@ -94,6 +95,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
)
filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False)
data_type = serializers.SerializerMethodField()
choice_set = NestedCustomFieldChoiceSetSerializer(required=False)
ui_visibility = ChoiceField(choices=CustomFieldVisibilityChoices, required=False)
class Meta:
@ -101,7 +103,7 @@ class CustomFieldSerializer(ValidatedModelSerializer):
fields = [
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'is_cloneable', 'default',
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created',
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choice_set', 'created',
'last_updated',
]
@ -127,6 +129,21 @@ class CustomFieldSerializer(ValidatedModelSerializer):
return 'string'
class CustomFieldChoiceSetSerializer(ValidatedModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='extras-api:customfieldchoiceset-detail')
base_choices = ChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
)
class Meta:
model = CustomFieldChoiceSet
fields = [
'id', 'url', 'display', 'name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically',
'choices_count', 'created', 'last_updated',
]
#
# Custom links
#

View File

@ -9,6 +9,7 @@ router.APIRootView = views.ExtrasRootView
router.register('webhooks', views.WebhookViewSet)
router.register('custom-fields', views.CustomFieldViewSet)
router.register('custom-field-choices', views.CustomFieldChoiceSetViewSet)
router.register('custom-links', views.CustomLinkViewSet)
router.register('export-templates', views.ExportTemplateViewSet)
router.register('saved-filters', views.SavedFilterViewSet)

View File

@ -6,7 +6,6 @@ from rest_framework import status
from rest_framework.decorators import action
from rest_framework.exceptions import PermissionDenied
from rest_framework.generics import RetrieveUpdateDestroyAPIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.renderers import JSONRenderer
from rest_framework.response import Response
from rest_framework.routers import APIRootView
@ -55,11 +54,37 @@ class WebhookViewSet(NetBoxModelViewSet):
class CustomFieldViewSet(NetBoxModelViewSet):
metadata_class = ContentTypeMetadata
queryset = CustomField.objects.all()
queryset = CustomField.objects.select_related('choice_set')
serializer_class = serializers.CustomFieldSerializer
filterset_class = filtersets.CustomFieldFilterSet
class CustomFieldChoiceSetViewSet(NetBoxModelViewSet):
queryset = CustomFieldChoiceSet.objects.all()
serializer_class = serializers.CustomFieldChoiceSetSerializer
filterset_class = filtersets.CustomFieldChoiceSetFilterSet
@action(detail=True)
def choices(self, request, pk):
"""
Provides an endpoint to iterate through each choice in a set.
"""
choiceset = get_object_or_404(self.queryset, pk=pk)
choices = choiceset.choices
# Enable filtering
if q := request.GET.get('q'):
q = q.lower()
choices = [c for c in choices if q in c[0].lower() or q in c[1].lower()]
# Paginate data
if page := self.paginate_queryset(choices):
data = [
{'value': c[0], 'label': c[1]} for c in page
]
return self.get_paginated_response(data)
#
# Custom links
#
@ -314,7 +339,7 @@ class ScriptViewSet(ViewSet):
# Attach Job objects to each script (if any)
for script in script_list:
script.result = results.get(script.name, None)
script.result = results.get(script.class_name, None)
serializer = serializers.ScriptSerializer(script_list, many=True, context={'request': request})
@ -325,7 +350,7 @@ class ScriptViewSet(ViewSet):
object_type = ContentType.objects.get(app_label='extras', model='scriptmodule')
script.result = Job.objects.filter(
object_type=object_type,
name=script.name,
name=script.class_name,
status__in=JobStatusChoices.TERMINAL_STATE_CHOICES
).first()
serializer = serializers.ScriptDetailSerializer(script, context={'request': request})
@ -392,7 +417,7 @@ class ContentTypeViewSet(ReadOnlyModelViewSet):
"""
Read-only list of ContentTypes. Limit results to ContentTypes pertinent to NetBox objects.
"""
permission_classes = (IsAuthenticated,)
permission_classes = [IsAuthenticatedOrLoginNotRequired]
queryset = ContentType.objects.order_by('app_label', 'model')
serializer_class = serializers.ContentTypeSerializer
filterset_class = filtersets.ContentTypeFilterSet

View File

@ -1,3 +1,5 @@
from django.utils.translation import gettext_lazy as _
from utilities.choices import ButtonColorChoices, ChoiceSet
@ -22,19 +24,19 @@ class CustomFieldTypeChoices(ChoiceSet):
TYPE_MULTIOBJECT = 'multiobject'
CHOICES = (
(TYPE_TEXT, 'Text'),
(TYPE_LONGTEXT, 'Text (long)'),
(TYPE_INTEGER, 'Integer'),
(TYPE_DECIMAL, 'Decimal'),
(TYPE_BOOLEAN, 'Boolean (true/false)'),
(TYPE_DATE, 'Date'),
(TYPE_DATETIME, 'Date & time'),
(TYPE_URL, 'URL'),
(TYPE_JSON, 'JSON'),
(TYPE_SELECT, 'Selection'),
(TYPE_MULTISELECT, 'Multiple selection'),
(TYPE_OBJECT, 'Object'),
(TYPE_MULTIOBJECT, 'Multiple objects'),
(TYPE_TEXT, _('Text')),
(TYPE_LONGTEXT, _('Text (long)')),
(TYPE_INTEGER, _('Integer')),
(TYPE_DECIMAL, _('Decimal')),
(TYPE_BOOLEAN, _('Boolean (true/false)')),
(TYPE_DATE, _('Date')),
(TYPE_DATETIME, _('Date & time')),
(TYPE_URL, _('URL')),
(TYPE_JSON, _('JSON')),
(TYPE_SELECT, _('Selection')),
(TYPE_MULTISELECT, _('Multiple selection')),
(TYPE_OBJECT, _('Object')),
(TYPE_MULTIOBJECT, _('Multiple objects')),
)
@ -45,9 +47,9 @@ class CustomFieldFilterLogicChoices(ChoiceSet):
FILTER_EXACT = 'exact'
CHOICES = (
(FILTER_DISABLED, 'Disabled'),
(FILTER_LOOSE, 'Loose'),
(FILTER_EXACT, 'Exact'),
(FILTER_DISABLED, _('Disabled')),
(FILTER_LOOSE, _('Loose')),
(FILTER_EXACT, _('Exact')),
)
@ -59,10 +61,23 @@ class CustomFieldVisibilityChoices(ChoiceSet):
VISIBILITY_HIDDEN_IFUNSET = 'hidden-ifunset'
CHOICES = (
(VISIBILITY_READ_WRITE, 'Read/Write'),
(VISIBILITY_READ_ONLY, 'Read-only'),
(VISIBILITY_HIDDEN, 'Hidden'),
(VISIBILITY_HIDDEN_IFUNSET, 'Hidden (if unset)'),
(VISIBILITY_READ_WRITE, _('Read/write')),
(VISIBILITY_READ_ONLY, _('Read-only')),
(VISIBILITY_HIDDEN, _('Hidden')),
(VISIBILITY_HIDDEN_IFUNSET, _('Hidden (if unset)')),
)
class CustomFieldChoiceSetBaseChoices(ChoiceSet):
IATA = 'IATA'
ISO_3166 = 'ISO_3166'
UN_LOCODE = 'UN_LOCODE'
CHOICES = (
(IATA, 'IATA (Airport codes)'),
(ISO_3166, 'ISO 3166 (Country codes)'),
(UN_LOCODE, 'UN/LOCODE (Location codes)'),
)
@ -76,7 +91,7 @@ class CustomLinkButtonClassChoices(ButtonColorChoices):
CHOICES = (
*ButtonColorChoices.CHOICES,
(LINK, 'Link'),
(LINK, _('Link')),
)
@ -90,8 +105,8 @@ class BookmarkOrderingChoices(ChoiceSet):
ORDERING_OLDEST = 'created'
CHOICES = (
(ORDERING_NEWEST, 'Newest'),
(ORDERING_OLDEST, 'Oldest'),
(ORDERING_NEWEST, _('Newest')),
(ORDERING_OLDEST, _('Oldest')),
)
#
@ -106,9 +121,9 @@ class ObjectChangeActionChoices(ChoiceSet):
ACTION_DELETE = 'delete'
CHOICES = (
(ACTION_CREATE, 'Created', 'green'),
(ACTION_UPDATE, 'Updated', 'blue'),
(ACTION_DELETE, 'Deleted', 'red'),
(ACTION_CREATE, _('Created'), 'green'),
(ACTION_UPDATE, _('Updated'), 'blue'),
(ACTION_DELETE, _('Deleted'), 'red'),
)
@ -125,10 +140,10 @@ class JournalEntryKindChoices(ChoiceSet):
KIND_DANGER = 'danger'
CHOICES = [
(KIND_INFO, 'Info', 'cyan'),
(KIND_SUCCESS, 'Success', 'green'),
(KIND_WARNING, 'Warning', 'yellow'),
(KIND_DANGER, 'Danger', 'red'),
(KIND_INFO, _('Info'), 'cyan'),
(KIND_SUCCESS, _('Success'), 'green'),
(KIND_WARNING, _('Warning'), 'yellow'),
(KIND_DANGER, _('Danger'), 'red'),
]
@ -145,22 +160,22 @@ class LogLevelChoices(ChoiceSet):
LOG_FAILURE = 'failure'
CHOICES = (
(LOG_DEFAULT, 'Default', 'gray'),
(LOG_SUCCESS, 'Success', 'green'),
(LOG_INFO, 'Info', 'cyan'),
(LOG_WARNING, 'Warning', 'yellow'),
(LOG_FAILURE, 'Failure', 'red'),
(LOG_DEFAULT, _('Default'), 'gray'),
(LOG_SUCCESS, _('Success'), 'green'),
(LOG_INFO, _('Info'), 'cyan'),
(LOG_WARNING, _('Warning'), 'yellow'),
(LOG_FAILURE, _('Failure'), 'red'),
)
class DurationChoices(ChoiceSet):
CHOICES = (
(60, 'Hourly'),
(720, '12 hours'),
(1440, 'Daily'),
(10080, 'Weekly'),
(43200, '30 days'),
(60, _('Hourly')),
(720, _('12 hours')),
(1440, _('Daily')),
(10080, _('Weekly')),
(43200, _('30 days')),
)
@ -178,12 +193,12 @@ class JobResultStatusChoices(ChoiceSet):
STATUS_FAILED = 'failed'
CHOICES = (
(STATUS_PENDING, 'Pending', 'cyan'),
(STATUS_SCHEDULED, 'Scheduled', 'gray'),
(STATUS_RUNNING, 'Running', 'blue'),
(STATUS_COMPLETED, 'Completed', 'green'),
(STATUS_ERRORED, 'Errored', 'red'),
(STATUS_FAILED, 'Failed', 'red'),
(STATUS_PENDING, _('Pending'), 'cyan'),
(STATUS_SCHEDULED, _('Scheduled'), 'gray'),
(STATUS_RUNNING, _('Running'), 'blue'),
(STATUS_COMPLETED, _('Completed'), 'green'),
(STATUS_ERRORED, _('Errored'), 'red'),
(STATUS_FAILED, _('Failed'), 'red'),
)
TERMINAL_STATE_CHOICES = (
@ -225,7 +240,7 @@ class ChangeActionChoices(ChoiceSet):
ACTION_DELETE = 'delete'
CHOICES = (
(ACTION_CREATE, 'Create', 'green'),
(ACTION_UPDATE, 'Update', 'blue'),
(ACTION_DELETE, 'Delete', 'red'),
(ACTION_CREATE, _('Create'), 'green'),
(ACTION_UPDATE, _('Update'), 'blue'),
(ACTION_DELETE, _('Delete'), 'red'),
)

View File

@ -0,0 +1,9 @@
from .iata import IATA
from .iso_3166 import ISO_3166
from .un_locode import UN_LOCODE
CHOICE_SETS = {
'IATA': IATA,
'ISO_3166': ISO_3166,
'UN_LOCODE': UN_LOCODE,
}

9768
netbox/extras/data/iata.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,253 @@
# Two-letter country codes defined by ISO 3166
# Source: https://datahub.io/core/country-list
ISO_3166 = [
('AD', 'AD (Andorra)'),
('AE', 'AE (United Arab Emirates)'),
('AF', 'AF (Afghanistan)'),
('AG', 'AG (Antigua and Barbuda)'),
('AI', 'AI (Anguilla)'),
('AL', 'AL (Albania)'),
('AM', 'AM (Armenia)'),
('AO', 'AO (Angola)'),
('AQ', 'AQ (Antarctica)'),
('AR', 'AR (Argentina)'),
('AS', 'AS (American Samoa)'),
('AT', 'AT (Austria)'),
('AU', 'AU (Australia)'),
('AW', 'AW (Aruba)'),
('AX', 'AX (Åland Islands)'),
('AZ', 'AZ (Azerbaijan)'),
('BA', 'BA (Bosnia and Herzegovina)'),
('BB', 'BB (Barbados)'),
('BD', 'BD (Bangladesh)'),
('BE', 'BE (Belgium)'),
('BF', 'BF (Burkina Faso)'),
('BG', 'BG (Bulgaria)'),
('BH', 'BH (Bahrain)'),
('BI', 'BI (Burundi)'),
('BJ', 'BJ (Benin)'),
('BL', 'BL (Saint Barthélemy)'),
('BM', 'BM (Bermuda)'),
('BN', 'BN (Brunei Darussalam)'),
('BO', 'BO (Bolivia, Plurinational State of)'),
('BQ', 'BQ (Bonaire, Sint Eustatius and Saba)'),
('BR', 'BR (Brazil)'),
('BS', 'BS (Bahamas)'),
('BT', 'BT (Bhutan)'),
('BV', 'BV (Bouvet Island)'),
('BW', 'BW (Botswana)'),
('BY', 'BY (Belarus)'),
('BZ', 'BZ (Belize)'),
('CA', 'CA (Canada)'),
('CC', 'CC (Cocos (Keeling) Islands)'),
('CD', 'CD (Congo, the Democratic Republic of the)'),
('CF', 'CF (Central African Republic)'),
('CG', 'CG (Congo)'),
('CH', 'CH (Switzerland)'),
('CI', "CI (Côte d'Ivoire)"),
('CK', 'CK (Cook Islands)'),
('CL', 'CL (Chile)'),
('CM', 'CM (Cameroon)'),
('CN', 'CN (China)'),
('CO', 'CO (Colombia)'),
('CR', 'CR (Costa Rica)'),
('CU', 'CU (Cuba)'),
('CV', 'CV (Cape Verde)'),
('CW', 'CW (Curaçao)'),
('CX', 'CX (Christmas Island)'),
('CY', 'CY (Cyprus)'),
('CZ', 'CZ (Czech Republic)'),
('DE', 'DE (Germany)'),
('DJ', 'DJ (Djibouti)'),
('DK', 'DK (Denmark)'),
('DM', 'DM (Dominica)'),
('DO', 'DO (Dominican Republic)'),
('DZ', 'DZ (Algeria)'),
('EC', 'EC (Ecuador)'),
('EE', 'EE (Estonia)'),
('EG', 'EG (Egypt)'),
('EH', 'EH (Western Sahara)'),
('ER', 'ER (Eritrea)'),
('ES', 'ES (Spain)'),
('ET', 'ET (Ethiopia)'),
('FI', 'FI (Finland)'),
('FJ', 'FJ (Fiji)'),
('FK', 'FK (Falkland Islands (Malvinas))'),
('FM', 'FM (Micronesia, Federated States of)'),
('FO', 'FO (Faroe Islands)'),
('FR', 'FR (France)'),
('GA', 'GA (Gabon)'),
('GB', 'GB (United Kingdom)'),
('GD', 'GD (Grenada)'),
('GE', 'GE (Georgia)'),
('GF', 'GF (French Guiana)'),
('GG', 'GG (Guernsey)'),
('GH', 'GH (Ghana)'),
('GI', 'GI (Gibraltar)'),
('GL', 'GL (Greenland)'),
('GM', 'GM (Gambia)'),
('GN', 'GN (Guinea)'),
('GP', 'GP (Guadeloupe)'),
('GQ', 'GQ (Equatorial Guinea)'),
('GR', 'GR (Greece)'),
('GS', 'GS (South Georgia and the South Sandwich Islands)'),
('GT', 'GT (Guatemala)'),
('GU', 'GU (Guam)'),
('GW', 'GW (Guinea-Bissau)'),
('GY', 'GY (Guyana)'),
('HK', 'HK (Hong Kong)'),
('HM', 'HM (Heard Island and McDonald Islands)'),
('HN', 'HN (Honduras)'),
('HR', 'HR (Croatia)'),
('HT', 'HT (Haiti)'),
('HU', 'HU (Hungary)'),
('ID', 'ID (Indonesia)'),
('IE', 'IE (Ireland)'),
('IL', 'IL (Israel)'),
('IM', 'IM (Isle of Man)'),
('IN', 'IN (India)'),
('IO', 'IO (British Indian Ocean Territory)'),
('IQ', 'IQ (Iraq)'),
('IR', 'IR (Iran, Islamic Republic of)'),
('IS', 'IS (Iceland)'),
('IT', 'IT (Italy)'),
('JE', 'JE (Jersey)'),
('JM', 'JM (Jamaica)'),
('JO', 'JO (Jordan)'),
('JP', 'JP (Japan)'),
('KE', 'KE (Kenya)'),
('KG', 'KG (Kyrgyzstan)'),
('KH', 'KH (Cambodia)'),
('KI', 'KI (Kiribati)'),
('KM', 'KM (Comoros)'),
('KN', 'KN (Saint Kitts and Nevis)'),
('KP', "KP (Korea, Democratic People's Republic of)"),
('KR', 'KR (Korea, Republic of)'),
('KW', 'KW (Kuwait)'),
('KY', 'KY (Cayman Islands)'),
('KZ', 'KZ (Kazakhstan)'),
('LA', "LA (Lao People's Democratic Republic)"),
('LB', 'LB (Lebanon)'),
('LC', 'LC (Saint Lucia)'),
('LI', 'LI (Liechtenstein)'),
('LK', 'LK (Sri Lanka)'),
('LR', 'LR (Liberia)'),
('LS', 'LS (Lesotho)'),
('LT', 'LT (Lithuania)'),
('LU', 'LU (Luxembourg)'),
('LV', 'LV (Latvia)'),
('LY', 'LY (Libya)'),
('MA', 'MA (Morocco)'),
('MC', 'MC (Monaco)'),
('MD', 'MD (Moldova, Republic of)'),
('ME', 'ME (Montenegro)'),
('MF', 'MF (Saint Martin (French part))'),
('MG', 'MG (Madagascar)'),
('MH', 'MH (Marshall Islands)'),
('MK', 'MK (Macedonia, the Former Yugoslav Republic of)'),
('ML', 'ML (Mali)'),
('MM', 'MM (Myanmar)'),
('MN', 'MN (Mongolia)'),
('MO', 'MO (Macao)'),
('MP', 'MP (Northern Mariana Islands)'),
('MQ', 'MQ (Martinique)'),
('MR', 'MR (Mauritania)'),
('MS', 'MS (Montserrat)'),
('MT', 'MT (Malta)'),
('MU', 'MU (Mauritius)'),
('MV', 'MV (Maldives)'),
('MW', 'MW (Malawi)'),
('MX', 'MX (Mexico)'),
('MY', 'MY (Malaysia)'),
('MZ', 'MZ (Mozambique)'),
('NA', 'NA (Namibia)'),
('NC', 'NC (New Caledonia)'),
('NE', 'NE (Niger)'),
('NF', 'NF (Norfolk Island)'),
('NG', 'NG (Nigeria)'),
('NI', 'NI (Nicaragua)'),
('NL', 'NL (Netherlands)'),
('NO', 'NO (Norway)'),
('NP', 'NP (Nepal)'),
('NR', 'NR (Nauru)'),
('NU', 'NU (Niue)'),
('NZ', 'NZ (New Zealand)'),
('OM', 'OM (Oman)'),
('PA', 'PA (Panama)'),
('PE', 'PE (Peru)'),
('PF', 'PF (French Polynesia)'),
('PG', 'PG (Papua New Guinea)'),
('PH', 'PH (Philippines)'),
('PK', 'PK (Pakistan)'),
('PL', 'PL (Poland)'),
('PM', 'PM (Saint Pierre and Miquelon)'),
('PN', 'PN (Pitcairn)'),
('PR', 'PR (Puerto Rico)'),
('PS', 'PS (Palestine, State of)'),
('PT', 'PT (Portugal)'),
('PW', 'PW (Palau)'),
('PY', 'PY (Paraguay)'),
('QA', 'QA (Qatar)'),
('RE', 'RE (Réunion)'),
('RO', 'RO (Romania)'),
('RS', 'RS (Serbia)'),
('RU', 'RU (Russian Federation)'),
('RW', 'RW (Rwanda)'),
('SA', 'SA (Saudi Arabia)'),
('SB', 'SB (Solomon Islands)'),
('SC', 'SC (Seychelles)'),
('SD', 'SD (Sudan)'),
('SE', 'SE (Sweden)'),
('SG', 'SG (Singapore)'),
('SH', 'SH (Saint Helena, Ascension and Tristan da Cunha)'),
('SI', 'SI (Slovenia)'),
('SJ', 'SJ (Svalbard and Jan Mayen)'),
('SK', 'SK (Slovakia)'),
('SL', 'SL (Sierra Leone)'),
('SM', 'SM (San Marino)'),
('SN', 'SN (Senegal)'),
('SO', 'SO (Somalia)'),
('SR', 'SR (Suriname)'),
('SS', 'SS (South Sudan)'),
('ST', 'ST (Sao Tome and Principe)'),
('SV', 'SV (El Salvador)'),
('SX', 'SX (Sint Maarten (Dutch part))'),
('SY', 'SY (Syrian Arab Republic)'),
('SZ', 'SZ (Swaziland)'),
('TC', 'TC (Turks and Caicos Islands)'),
('TD', 'TD (Chad)'),
('TF', 'TF (French Southern Territories)'),
('TG', 'TG (Togo)'),
('TH', 'TH (Thailand)'),
('TJ', 'TJ (Tajikistan)'),
('TK', 'TK (Tokelau)'),
('TL', 'TL (Timor-Leste)'),
('TM', 'TM (Turkmenistan)'),
('TN', 'TN (Tunisia)'),
('TO', 'TO (Tonga)'),
('TR', 'TR (Turkey)'),
('TT', 'TT (Trinidad and Tobago)'),
('TV', 'TV (Tuvalu)'),
('TW', 'TW (Taiwan, Province of China)'),
('TZ', 'TZ (Tanzania, United Republic of)'),
('UA', 'UA (Ukraine)'),
('UG', 'UG (Uganda)'),
('UM', 'UM (United States Minor Outlying Islands)'),
('US', 'US (United States)'),
('UY', 'UY (Uruguay)'),
('UZ', 'UZ (Uzbekistan)'),
('VA', 'VA (Holy See (Vatican City State))'),
('VC', 'VC (Saint Vincent and the Grenadines)'),
('VE', 'VE (Venezuela, Bolivarian Republic of)'),
('VG', 'VG (Virgin Islands, British)'),
('VI', 'VI (Virgin Islands, U.S.)'),
('VN', 'VN (Viet Nam)'),
('VU', 'VU (Vanuatu)'),
('WF', 'WF (Wallis and Futuna)'),
('WS', 'WS (Samoa)'),
('YE', 'YE (Yemen)'),
('YT', 'YT (Mayotte)'),
('ZA', 'ZA (South Africa)'),
('ZM', 'ZM (Zambia)'),
('ZW', 'ZW (Zimbabwe)')
]

111557
netbox/extras/data/un_locode.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@ __all__ = (
'ConfigRevisionFilterSet',
'ConfigTemplateFilterSet',
'ContentTypeFilterSet',
'CustomFieldChoiceSetFilterSet',
'CustomFieldFilterSet',
'CustomLinkFilterSet',
'ExportTemplateFilterSet',
@ -74,6 +75,14 @@ class CustomFieldFilterSet(BaseFilterSet):
field_name='content_types__id'
)
content_types = ContentTypeFilter()
choice_set_id = django_filters.ModelMultipleChoiceFilter(
queryset=CustomFieldChoiceSet.objects.all()
)
choice_set = django_filters.ModelMultipleChoiceFilter(
field_name='choice_set__name',
queryset=CustomFieldChoiceSet.objects.all(),
to_field_name='name'
)
class Meta:
model = CustomField
@ -93,6 +102,35 @@ class CustomFieldFilterSet(BaseFilterSet):
)
class CustomFieldChoiceSetFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',
label=_('Search'),
)
choice = MultiValueCharFilter(
method='filter_by_choice'
)
class Meta:
model = CustomFieldChoiceSet
fields = [
'id', 'name', 'description', 'base_choices', 'order_alphabetically',
]
def search(self, queryset, name, value):
if not value.strip():
return queryset
return queryset.filter(
Q(name__icontains=value) |
Q(description__icontains=value) |
Q(extra_choices__contains=value)
)
def filter_by_choice(self, queryset, name, value):
# TODO: Support case-insensitive matching
return queryset.filter(extra_choices__overlap=value)
class CustomLinkFilterSet(BaseFilterSet):
q = django_filters.CharFilter(
method='search',

View File

@ -1,16 +1,17 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from extras.choices import *
from extras.models import *
from utilities.forms import BulkEditForm, add_blank_choice
from utilities.forms.fields import ColorField
from utilities.forms.fields import ColorField, DynamicModelChoiceField
from utilities.forms.widgets import BulkEditNullBooleanSelect
__all__ = (
'ConfigContextBulkEditForm',
'ConfigTemplateBulkEditForm',
'CustomFieldBulkEditForm',
'CustomFieldChoiceSetBulkEditForm',
'CustomLinkBulkEditForm',
'ExportTemplateBulkEditForm',
'JournalEntryBulkEditForm',
@ -26,16 +27,24 @@ class CustomFieldBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
group_name = forms.CharField(
label=_('Group name'),
required=False
)
description = forms.CharField(
label=_('Description'),
required=False
)
required = forms.NullBooleanField(
label=_('Required'),
required=False,
widget=BulkEditNullBooleanSelect()
)
weight = forms.IntegerField(
label=_('Weight'),
required=False
)
choice_set = DynamicModelChoiceField(
queryset=CustomFieldChoiceSet.objects.all(),
required=False
)
ui_visibility = forms.ChoiceField(
@ -45,11 +54,32 @@ class CustomFieldBulkEditForm(BulkEditForm):
initial=''
)
is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
required=False,
widget=BulkEditNullBooleanSelect()
)
nullable_fields = ('group_name', 'description',)
nullable_fields = ('group_name', 'description', 'choice_set')
class CustomFieldChoiceSetBulkEditForm(BulkEditForm):
pk = forms.ModelMultipleChoiceField(
queryset=CustomFieldChoiceSet.objects.all(),
widget=forms.MultipleHiddenInput
)
base_choices = forms.ChoiceField(
choices=add_blank_choice(CustomFieldChoiceSetBaseChoices),
required=False
)
description = forms.CharField(
required=False
)
order_alphabetically = forms.NullBooleanField(
required=False,
widget=BulkEditNullBooleanSelect()
)
nullable_fields = ('base_choices', 'description')
class CustomLinkBulkEditForm(BulkEditForm):
@ -58,17 +88,21 @@ class CustomLinkBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=BulkEditNullBooleanSelect()
)
new_window = forms.NullBooleanField(
label=_('New window'),
required=False,
widget=BulkEditNullBooleanSelect()
)
weight = forms.IntegerField(
label=_('Weight'),
required=False
)
button_class = forms.ChoiceField(
label=_('Button class'),
choices=add_blank_choice(CustomLinkButtonClassChoices),
required=False
)
@ -80,18 +114,22 @@ class ExportTemplateBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
mime_type = forms.CharField(
label=_('MIME type'),
max_length=50,
required=False
)
file_extension = forms.CharField(
label=_('File extension'),
max_length=15,
required=False
)
as_attachment = forms.NullBooleanField(
label=_('As attachment'),
required=False,
widget=BulkEditNullBooleanSelect()
)
@ -105,17 +143,21 @@ class SavedFilterBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
weight = forms.IntegerField(
label=_('Weight'),
required=False
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=BulkEditNullBooleanSelect()
)
shared = forms.NullBooleanField(
label=_('Shared'),
required=False,
widget=BulkEditNullBooleanSelect()
)
@ -129,26 +171,32 @@ class WebhookBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=BulkEditNullBooleanSelect()
)
type_create = forms.NullBooleanField(
label=_('On create'),
required=False,
widget=BulkEditNullBooleanSelect()
)
type_update = forms.NullBooleanField(
label=_('On update'),
required=False,
widget=BulkEditNullBooleanSelect()
)
type_delete = forms.NullBooleanField(
label=_('On delete'),
required=False,
widget=BulkEditNullBooleanSelect()
)
type_job_start = forms.NullBooleanField(
label=_('On job start'),
required=False,
widget=BulkEditNullBooleanSelect()
)
type_job_end = forms.NullBooleanField(
label=_('On job end'),
required=False,
widget=BulkEditNullBooleanSelect()
)
@ -167,6 +215,7 @@ class WebhookBulkEditForm(BulkEditForm):
label=_('SSL verification')
)
secret = forms.CharField(
label=_('Secret'),
required=False
)
ca_file_path = forms.CharField(
@ -183,9 +232,11 @@ class TagBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
color = ColorField(
label=_('Color'),
required=False
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
@ -199,14 +250,17 @@ class ConfigContextBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
weight = forms.IntegerField(
label=_('Weight'),
required=False,
min_value=0
)
is_active = forms.NullBooleanField(
label=_('Is active'),
required=False,
widget=BulkEditNullBooleanSelect()
)
description = forms.CharField(
label=_('Description'),
required=False,
max_length=100
)
@ -220,6 +274,7 @@ class ConfigTemplateBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
description = forms.CharField(
label=_('Description'),
max_length=200,
required=False
)
@ -233,10 +288,12 @@ class JournalEntryBulkEditForm(BulkEditForm):
widget=forms.MultipleHiddenInput
)
kind = forms.ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices),
required=False
)
comments = forms.CharField(
label=_('Comments'),
required=False,
widget=forms.Textarea()
)

View File

@ -2,17 +2,20 @@ from django import forms
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices, JournalEntryKindChoices
from extras.choices import *
from extras.models import *
from extras.utils import FeatureQuery
from netbox.forms import NetBoxModelImportForm
from utilities.forms import CSVModelForm
from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVMultipleContentTypeField, SlugField
from utilities.forms.fields import (
CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVMultipleContentTypeField, SlugField,
)
__all__ = (
'ConfigTemplateImportForm',
'CustomFieldChoiceSetImportForm',
'CustomFieldImportForm',
'CustomLinkImportForm',
'ExportTemplateImportForm',
@ -25,26 +28,32 @@ __all__ = (
class CustomFieldImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
help_text=_("One or more assigned object types")
)
type = CSVChoiceField(
label=_('Type'),
choices=CustomFieldTypeChoices,
help_text=_('Field data type (e.g. text, integer, etc.)')
)
object_type = CSVContentTypeField(
label=_('Object type'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
required=False,
help_text=_("Object type (for object or multi-object fields)")
)
choices = SimpleArrayField(
base_field=forms.CharField(),
choice_set = CSVModelChoiceField(
label=_('Choice set'),
queryset=CustomFieldChoiceSet.objects.all(),
to_field_name='name',
required=False,
help_text=_('Comma-separated list of field choices')
help_text=_('Choice set (for selection fields)')
)
ui_visibility = CSVChoiceField(
label=_('UI visibility'),
choices=CustomFieldVisibilityChoices,
help_text=_('How the custom field is displayed in the user interface')
)
@ -53,13 +62,33 @@ class CustomFieldImportForm(CSVModelForm):
model = CustomField
fields = (
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description',
'search_weight', 'filter_logic', 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum',
'validation_regex', 'ui_visibility', 'is_cloneable',
'search_weight', 'filter_logic', 'default', 'choice_set', 'weight', 'validation_minimum',
'validation_maximum', 'validation_regex', 'ui_visibility', 'is_cloneable',
)
class CustomFieldChoiceSetImportForm(CSVModelForm):
base_choices = CSVChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False,
help_text=_('The base set of predefined choices to use (if any)')
)
extra_choices = SimpleArrayField(
base_field=forms.CharField(),
required=False,
help_text=_('Comma-separated list of field choices')
)
class Meta:
model = CustomFieldChoiceSet
fields = (
'name', 'description', 'extra_choices', 'order_alphabetically',
)
class CustomLinkImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links'),
help_text=_("One or more assigned object types")
@ -75,6 +104,7 @@ class CustomLinkImportForm(CSVModelForm):
class ExportTemplateImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates'),
help_text=_("One or more assigned object types")
@ -98,6 +128,7 @@ class ConfigTemplateImportForm(CSVModelForm):
class SavedFilterImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
help_text=_("One or more assigned object types")
)
@ -111,6 +142,7 @@ class SavedFilterImportForm(CSVModelForm):
class WebhookImportForm(CSVModelForm):
content_types = CSVMultipleContentTypeField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks'),
help_text=_("One or more assigned object types")
@ -142,6 +174,7 @@ class JournalEntryImportForm(NetBoxModelImportForm):
label=_('Assigned object type'),
)
kind = CSVChoiceField(
label=_('Kind'),
choices=JournalEntryKindChoices,
help_text=_('The classification of entry')
)

View File

@ -1,7 +1,7 @@
from django import forms
from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from core.models import DataFile, DataSource
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@ -11,7 +11,9 @@ from extras.utils import FeatureQuery
from netbox.forms.base import NetBoxModelFilterSetForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice
from utilities.forms.fields import ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField
from utilities.forms.fields import (
ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField,
)
from utilities.forms.widgets import APISelectMultiple, DateTimePicker
from virtualization.models import Cluster, ClusterGroup, ClusterType
from .mixins import SavedFiltersMixin
@ -20,6 +22,7 @@ __all__ = (
'ConfigContextFilterForm',
'ConfigRevisionFilterForm',
'ConfigTemplateFilterForm',
'CustomFieldChoiceSetFilterForm',
'CustomFieldFilterForm',
'CustomLinkFilterForm',
'ExportTemplateFilterForm',
@ -36,8 +39,9 @@ __all__ = (
class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', (
'type', 'content_type_id', 'group_name', 'weight', 'required', 'ui_visibility', 'is_cloneable',
(_('Attributes'), (
'type', 'content_type_id', 'group_name', 'weight', 'required', 'choice_set_id', 'ui_visibility',
'is_cloneable',
)),
)
content_type_id = ContentTypeMultipleChoiceField(
@ -51,23 +55,32 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
label=_('Field type')
)
group_name = forms.CharField(
label=_('Group name'),
required=False
)
weight = forms.IntegerField(
label=_('Weight'),
required=False
)
required = forms.NullBooleanField(
label=_('Required'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
choice_set_id = DynamicModelMultipleChoiceField(
queryset=CustomFieldChoiceSet.objects.all(),
required=False,
label=_('Choice set')
)
ui_visibility = forms.ChoiceField(
choices=add_blank_choice(CustomFieldVisibilityChoices),
required=False,
label=_('UI visibility')
)
is_cloneable = forms.NullBooleanField(
label=_('Is cloneable'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
@ -75,28 +88,46 @@ class CustomFieldFilterForm(SavedFiltersMixin, FilterForm):
)
class CustomFieldChoiceSetFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
(_('Choices'), ('base_choices', 'choice')),
)
base_choices = forms.MultipleChoiceField(
choices=CustomFieldChoiceSetBaseChoices,
required=False
)
choice = forms.CharField(
required=False
)
class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', ('content_types', 'enabled', 'new_window', 'weight')),
(_('Attributes'), ('content_types', 'enabled', 'new_window', 'weight')),
)
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.filter(FeatureQuery('custom_links').get_query()),
required=False
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
new_window = forms.NullBooleanField(
label=_('New window'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
weight = forms.IntegerField(
label=_('Weight'),
required=False
)
@ -104,8 +135,8 @@ class CustomLinkFilterForm(SavedFiltersMixin, FilterForm):
class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Data', ('data_source_id', 'data_file_id')),
('Attributes', ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
(_('Data'), ('data_source_id', 'data_file_id')),
(_('Attributes'), ('content_types', 'mime_type', 'file_extension', 'as_attachment')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -121,6 +152,7 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
}
)
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
required=False
)
@ -129,9 +161,11 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
label=_('MIME type')
)
file_extension = forms.CharField(
label=_('File extension'),
required=False
)
as_attachment = forms.NullBooleanField(
label=_('As attachment'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
@ -142,13 +176,15 @@ class ExportTemplateFilterForm(SavedFiltersMixin, FilterForm):
class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', ('content_type_id', 'name',)),
(_('Attributes'), ('content_type_id', 'name',)),
)
content_type_id = ContentTypeChoiceField(
label=_('Content type'),
queryset=ContentType.objects.filter(FeatureQuery('custom_fields').get_query()),
required=False
)
name = forms.CharField(
label=_('Name'),
required=False
)
@ -156,25 +192,29 @@ class ImageAttachmentFilterForm(SavedFiltersMixin, FilterForm):
class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', ('content_types', 'enabled', 'shared', 'weight')),
(_('Attributes'), ('content_types', 'enabled', 'shared', 'weight')),
)
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()),
required=False
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
shared = forms.NullBooleanField(
label=_('Shared'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
)
)
weight = forms.IntegerField(
label=_('Weight'),
required=False
)
@ -182,8 +222,8 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
class WebhookFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id')),
('Attributes', ('content_type_id', 'http_method', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
(_('Attributes'), ('content_type_id', 'http_method', 'enabled')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
)
content_type_id = ContentTypeMultipleChoiceField(
queryset=ContentType.objects.filter(FeatureQuery('webhooks').get_query()),
@ -196,6 +236,7 @@ class WebhookFilterForm(SavedFiltersMixin, FilterForm):
label=_('HTTP method')
)
enabled = forms.NullBooleanField(
label=_('Enabled'),
required=False,
widget=forms.Select(
choices=BOOLEAN_WITH_BLANK_CHOICES
@ -255,11 +296,11 @@ class TagFilterForm(SavedFiltersMixin, FilterForm):
class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag_id')),
('Data', ('data_source_id', 'data_file_id')),
('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')),
('Device', ('device_type_id', 'platform_id', 'role_id')),
('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
('Tenant', ('tenant_group_id', 'tenant_id'))
(_('Data'), ('data_source_id', 'data_file_id')),
(_('Location'), ('region_id', 'site_group_id', 'site_id', 'location_id')),
(_('Device'), ('device_type_id', 'platform_id', 'role_id')),
(_('Cluster'), ('cluster_type_id', 'cluster_group_id', 'cluster_id')),
(_('Tenant'), ('tenant_group_id', 'tenant_id'))
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -345,7 +386,7 @@ class ConfigContextFilterForm(SavedFiltersMixin, FilterForm):
class ConfigTemplateFilterForm(SavedFiltersMixin, FilterForm):
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Data', ('data_source_id', 'data_file_id')),
(_('Data'), ('data_source_id', 'data_file_id')),
)
data_source_id = DynamicModelMultipleChoiceField(
queryset=DataSource.objects.all(),
@ -377,8 +418,8 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
model = JournalEntry
fieldsets = (
(None, ('q', 'filter_id', 'tag')),
('Creation', ('created_before', 'created_after', 'created_by_id')),
('Attributes', ('assigned_object_type_id', 'kind'))
(_('Creation'), ('created_before', 'created_after', 'created_by_id')),
(_('Attributes'), ('assigned_object_type_id', 'kind'))
)
created_after = forms.DateTimeField(
required=False,
@ -407,6 +448,7 @@ class JournalEntryFilterForm(NetBoxModelFilterSetForm):
)
)
kind = forms.ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices),
required=False
)
@ -417,8 +459,8 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
model = ObjectChange
fieldsets = (
(None, ('q', 'filter_id')),
('Time', ('time_before', 'time_after')),
('Attributes', ('action', 'user_id', 'changed_object_type_id')),
(_('Time'), ('time_before', 'time_after')),
(_('Attributes'), ('action', 'user_id', 'changed_object_type_id')),
)
time_after = forms.DateTimeField(
required=False,
@ -431,6 +473,7 @@ class ObjectChangeFilterForm(SavedFiltersMixin, FilterForm):
widget=DateTimePicker()
)
action = forms.ChoiceField(
label=_('Action'),
choices=add_blank_choice(ObjectChangeActionChoices),
required=False
)

View File

@ -1,4 +1,5 @@
from django import forms
from django.utils.translation import gettext_lazy as _
__all__ = (
'RenderMarkdownForm',
@ -10,5 +11,6 @@ class RenderMarkdownForm(forms.Form):
Provides basic validation for markup to be rendered.
"""
text = forms.CharField(
label=_('Text'),
required=False
)

View File

@ -4,7 +4,7 @@ from django import forms
from django.conf import settings
from django.db.models import Q
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
@ -16,9 +16,10 @@ from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup
from utilities.forms import BootstrapMixin, add_blank_choice
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField,
SlugField,
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField,
)
from utilities.forms.widgets import ChoicesWidget
from virtualization.models import Cluster, ClusterGroup, ClusterType
@ -27,6 +28,7 @@ __all__ = (
'ConfigContextForm',
'ConfigRevisionForm',
'ConfigTemplateForm',
'CustomFieldChoiceSetForm',
'CustomFieldForm',
'CustomLinkForm',
'ExportTemplateForm',
@ -40,24 +42,30 @@ __all__ = (
class CustomFieldForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_fields'),
)
object_type = ContentTypeChoiceField(
label=_('Object type'),
queryset=ContentType.objects.all(),
# TODO: Come up with a canonical way to register suitable models
limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']),
required=False,
help_text=_("Type of the related object (for object/multi-object fields only)")
)
choice_set = DynamicModelChoiceField(
queryset=CustomFieldChoiceSet.objects.all(),
required=False
)
fieldsets = (
('Custom Field', (
(_('Custom Field'), (
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
)),
('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
('Values', ('default', 'choices')),
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
(_('Behavior'), ('search_weight', 'filter_logic', 'ui_visibility', 'weight', 'is_cloneable')),
(_('Values'), ('default', 'choice_set')),
(_('Validation'), ('validation_minimum', 'validation_maximum', 'validation_regex')),
)
class Meta:
@ -78,15 +86,36 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
self.fields['type'].disabled = True
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
extra_choices = forms.CharField(
widget=ChoicesWidget(),
)
class Meta:
model = CustomFieldChoiceSet
fields = ('name', 'description', 'base_choices', 'extra_choices', 'order_alphabetically')
def clean_extra_choices(self):
data = []
for line in self.cleaned_data['extra_choices'].splitlines():
try:
value, label = line.split(',', maxsplit=1)
except ValueError:
value, label = line, line
data.append((value, label))
return data
class CustomLinkForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('custom_links')
)
fieldsets = (
('Custom Link', ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
('Templates', ('link_text', 'link_url')),
(_('Custom Link'), ('name', 'content_types', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')),
(_('Templates'), ('link_text', 'link_url')),
)
class Meta:
@ -107,18 +136,20 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('export_templates')
)
template_code = forms.CharField(
label=_('Template code'),
required=False,
widget=forms.Textarea(attrs={'class': 'font-monospace'})
)
fieldsets = (
('Export Template', ('name', 'content_types', 'description', 'template_code')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
('Rendering', ('mime_type', 'file_extension', 'as_attachment')),
(_('Export Template'), ('name', 'content_types', 'description', 'template_code')),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
(_('Rendering'), ('mime_type', 'file_extension', 'as_attachment')),
)
class Meta:
@ -139,7 +170,7 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
super().clean()
if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
raise forms.ValidationError("Must specify either local content or a data file")
raise forms.ValidationError(_("Must specify either local content or a data file"))
return self.cleaned_data
@ -147,13 +178,14 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
class SavedFilterForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all()
)
parameters = JSONField()
fieldsets = (
('Saved Filter', ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
('Parameters', ('parameters',)),
(_('Saved Filter'), ('name', 'slug', 'content_types', 'description', 'weight', 'enabled', 'shared')),
(_('Parameters'), ('parameters',)),
)
class Meta:
@ -172,6 +204,7 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
class BookmarkForm(BootstrapMixin, forms.ModelForm):
object_type = ContentTypeChoiceField(
label=_('Object type'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('bookmarks').get_query()
)
@ -183,29 +216,30 @@ class BookmarkForm(BootstrapMixin, forms.ModelForm):
class WebhookForm(BootstrapMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('webhooks')
)
fieldsets = (
('Webhook', ('name', 'content_types', 'enabled')),
('Events', ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
('HTTP Request', (
(_('Webhook'), ('name', 'content_types', 'enabled')),
(_('Events'), ('type_create', 'type_update', 'type_delete', 'type_job_start', 'type_job_end')),
(_('HTTP Request'), (
'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret',
)),
('Conditions', ('conditions',)),
('SSL', ('ssl_verification', 'ca_file_path')),
(_('Conditions'), ('conditions',)),
(_('SSL'), ('ssl_verification', 'ca_file_path')),
)
class Meta:
model = Webhook
fields = '__all__'
labels = {
'type_create': 'Creations',
'type_update': 'Updates',
'type_delete': 'Deletions',
'type_job_start': 'Job executions',
'type_job_end': 'Job terminations',
'type_create': _('Creations'),
'type_update': _('Updates'),
'type_delete': _('Deletions'),
'type_job_start': _('Job executions'),
'type_job_end': _('Job terminations'),
}
widgets = {
'additional_headers': forms.Textarea(attrs={'class': 'font-monospace'}),
@ -217,6 +251,7 @@ class WebhookForm(BootstrapMixin, forms.ModelForm):
class TagForm(BootstrapMixin, forms.ModelForm):
slug = SlugField()
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
queryset=ContentType.objects.all(),
limit_choices_to=FeatureQuery('tags'),
required=False
@ -235,65 +270,79 @@ class TagForm(BootstrapMixin, forms.ModelForm):
class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
regions = DynamicModelMultipleChoiceField(
label=_('Regions'),
queryset=Region.objects.all(),
required=False
)
site_groups = DynamicModelMultipleChoiceField(
label=_('Site groups'),
queryset=SiteGroup.objects.all(),
required=False
)
sites = DynamicModelMultipleChoiceField(
label=_('Sites'),
queryset=Site.objects.all(),
required=False
)
locations = DynamicModelMultipleChoiceField(
label=_('Locations'),
queryset=Location.objects.all(),
required=False
)
device_types = DynamicModelMultipleChoiceField(
label=_('Device types'),
queryset=DeviceType.objects.all(),
required=False
)
roles = DynamicModelMultipleChoiceField(
label=_('Roles'),
queryset=DeviceRole.objects.all(),
required=False
)
platforms = DynamicModelMultipleChoiceField(
label=_('Platforms'),
queryset=Platform.objects.all(),
required=False
)
cluster_types = DynamicModelMultipleChoiceField(
label=_('Cluster types'),
queryset=ClusterType.objects.all(),
required=False
)
cluster_groups = DynamicModelMultipleChoiceField(
label=_('Cluster groups'),
queryset=ClusterGroup.objects.all(),
required=False
)
clusters = DynamicModelMultipleChoiceField(
label=_('Clusters'),
queryset=Cluster.objects.all(),
required=False
)
tenant_groups = DynamicModelMultipleChoiceField(
label=_('Tenat groups'),
queryset=TenantGroup.objects.all(),
required=False
)
tenants = DynamicModelMultipleChoiceField(
label=_('Tenants'),
queryset=Tenant.objects.all(),
required=False
)
tags = DynamicModelMultipleChoiceField(
label=_('Tags'),
queryset=Tag.objects.all(),
required=False
)
data = JSONField(
label=_('Data'),
required=False
)
fieldsets = (
('Config Context', ('name', 'weight', 'description', 'data', 'is_active')),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
('Assignment', (
(_('Config Context'), ('name', 'weight', 'description', 'data', 'is_active')),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
(_('Assignment'), (
'regions', 'site_groups', 'sites', 'locations', 'device_types', 'roles', 'platforms', 'cluster_types',
'cluster_groups', 'clusters', 'tenant_groups', 'tenants', 'tags',
)),
@ -325,25 +374,27 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
super().clean()
if not self.cleaned_data.get('data') and not self.cleaned_data.get('data_file'):
raise forms.ValidationError("Must specify either local data or a data file")
raise forms.ValidationError(_("Must specify either local data or a data file"))
return self.cleaned_data
class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
tags = DynamicModelMultipleChoiceField(
label=_('Tags'),
queryset=Tag.objects.all(),
required=False
)
template_code = forms.CharField(
label=_('Template code'),
required=False,
widget=forms.Textarea(attrs={'class': 'font-monospace'})
)
fieldsets = (
('Config Template', ('name', 'description', 'environment_params', 'tags')),
('Content', ('template_code',)),
('Data Source', ('data_source', 'data_file', 'auto_sync_enabled')),
(_('Config Template'), ('name', 'description', 'environment_params', 'tags')),
(_('Content'), ('template_code',)),
(_('Data Source'), ('data_source', 'data_file', 'auto_sync_enabled')),
)
class Meta:
@ -367,7 +418,7 @@ class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
super().clean()
if not self.cleaned_data.get('template_code') and not self.cleaned_data.get('data_file'):
raise forms.ValidationError("Must specify either local content or a data file")
raise forms.ValidationError(_("Must specify either local content or a data file"))
return self.cleaned_data
@ -383,6 +434,7 @@ class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
class JournalEntryForm(NetBoxModelForm):
kind = forms.ChoiceField(
label=_('Kind'),
choices=add_blank_choice(JournalEntryKindChoices),
required=False
)
@ -425,16 +477,16 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
"""
fieldsets = (
('Rack Elevations', ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
('Power', ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
('IPAM', ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
('Security', ('ALLOWED_URL_SCHEMES',)),
('Banners', ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
('Pagination', ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
('Validation', ('CUSTOM_VALIDATORS',)),
('User Preferences', ('DEFAULT_USER_PREFERENCES',)),
('Miscellaneous', ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
('Config Revision', ('comment',))
(_('Rack Elevations'), ('RACK_ELEVATION_DEFAULT_UNIT_HEIGHT', 'RACK_ELEVATION_DEFAULT_UNIT_WIDTH')),
(_('Power'), ('POWERFEED_DEFAULT_VOLTAGE', 'POWERFEED_DEFAULT_AMPERAGE', 'POWERFEED_DEFAULT_MAX_UTILIZATION')),
(_('IPAM'), ('ENFORCE_GLOBAL_UNIQUE', 'PREFER_IPV4')),
(_('Security'), ('ALLOWED_URL_SCHEMES',)),
(_('Banners'), ('BANNER_LOGIN', 'BANNER_MAINTENANCE', 'BANNER_TOP', 'BANNER_BOTTOM')),
(_('Pagination'), ('PAGINATE_COUNT', 'MAX_PAGE_SIZE')),
(_('Validation'), ('CUSTOM_VALIDATORS',)),
(_('User Preferences'), ('DEFAULT_USER_PREFERENCES',)),
(_('Miscellaneous'), ('MAINTENANCE_MODE', 'GRAPHQL_ENABLED', 'CHANGELOG_RETENTION', 'JOB_RETENTION', 'MAPS_URL')),
(_('Config Revision'), ('comment',))
)
class Meta:
@ -461,11 +513,11 @@ class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMe
help_text = self.fields[param.name].help_text
if help_text:
help_text += '<br />' # Line break
help_text += f'Current value: <strong>{value}</strong>'
help_text += _('Current value: <strong>{value}</strong>').format(value=value)
if is_static:
help_text += ' (defined statically)'
help_text += _(' (defined statically)')
elif value == param.default:
help_text += ' (default)'
help_text += _(' (default)')
self.fields[param.name].help_text = help_text
self.fields[param.name].initial = value
if is_static:

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin
@ -33,7 +33,7 @@ class ReportForm(BootstrapMixin, forms.Form):
# Annotate the current system time for reference
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
self.fields['schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'
self.fields['schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
# Remove scheduling fields if scheduling is disabled
if not scheduling_enabled:

View File

@ -1,5 +1,5 @@
from django import forms
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin
@ -39,7 +39,7 @@ class ScriptForm(BootstrapMixin, forms.Form):
# Annotate the current system time for reference
now = local_now().strftime('%Y-%m-%d %H:%M:%S')
self.fields['_schedule_at'].help_text += f' (current time: <strong>{now}</strong>)'
self.fields['_schedule_at'].help_text += _(' (current time: <strong>{now}</strong>)').format(now=now)
# Remove scheduling fields if scheduling is disabled
if not scheduling_enabled:
@ -56,10 +56,3 @@ class ScriptForm(BootstrapMixin, forms.Form):
self.cleaned_data['_schedule_at'] = local_now()
return self.cleaned_data
@property
def requires_input(self):
"""
A boolean indicating whether the form requires user input (ignore the built-in fields).
"""
return bool(len(self.fields) > 3)

Some files were not shown because too many files have changed in this diff Show More