mirror of
https://github.com/netbox-community/netbox.git
synced 2026-03-21 20:18:38 -06:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8816af1389 | ||
|
|
ca76d37ffe | ||
|
|
ef0b0eaee9 | ||
|
|
8391c6ae95 | ||
|
|
0752fd2c63 | ||
|
|
ddb8ce90eb | ||
|
|
6193ef506f | ||
|
|
91e0b661a4 | ||
|
|
cef3eb0ab0 | ||
|
|
d314dac470 | ||
|
|
6294a96199 | ||
|
|
21f78049bc | ||
|
|
e28ed7446c | ||
|
|
9b57512b12 | ||
|
|
da79cc775d | ||
|
|
6f5fd26183 | ||
|
|
10157394ae | ||
|
|
ae0907fb37 | ||
|
|
fea6ad61fd | ||
|
|
675e68f276 | ||
|
|
20b907a8c9 | ||
|
|
8ccb0f7b63 | ||
|
|
068fce4d7c | ||
|
|
2e4bce2dad | ||
|
|
dad96c525f | ||
|
|
cac3c1221c | ||
|
|
3a9d00a537 | ||
|
|
4040e4f266 | ||
|
|
5e57cec369 | ||
|
|
f938309ed9 | ||
|
|
86f6de40d2 | ||
|
|
83c6149e49 | ||
|
|
b19d0d61f4 | ||
|
|
d64c4d75f8 | ||
|
|
9b0c6110bb | ||
|
|
c86210f024 | ||
|
|
1be917fb90 |
Vendored
+25
-7
@@ -30,16 +30,34 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 1
|
||||
|
||||
# Workaround for claude-code-action bug with fork PRs: The action tries to fetch by branch name, which doesn't
|
||||
# exist on origin for forks. Pre-fetch the PR ref so it's available as a local ref.
|
||||
- name: Fetch fork PR ref (if applicable)
|
||||
if: github.event.issue.pull_request != '' && github.event.issue.pull_request != null
|
||||
# Workaround for claude-code-action bug with fork PRs: The action fetches by branch name
|
||||
# (git fetch origin --depth=N <branch>), but fork PR branches don't exist on origin.
|
||||
# Fix: redirect origin to the fork's URL so the action can fetch the branch directly.
|
||||
- name: Configure git remote for fork PRs
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
PR_NUMBER=$(gh pr view ${{ github.event.issue.number }} --json number -q .number 2>/dev/null || echo "")
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
git fetch origin refs/pull/${PR_NUMBER}/head:refs/remotes/pull/${PR_NUMBER}/head || true
|
||||
# Determine PR number based on event type
|
||||
if [ "${{ github.event_name }}" = "issue_comment" ]; then
|
||||
PR_NUMBER="${{ github.event.issue.number }}"
|
||||
elif [ "${{ github.event_name }}" = "pull_request_review_comment" ] || [ "${{ github.event_name }}" = "pull_request_review" ]; then
|
||||
PR_NUMBER="${{ github.event.pull_request.number }}"
|
||||
else
|
||||
exit 0 # issues event — no PR branch to worry about
|
||||
fi
|
||||
|
||||
# Fetch fork info in one API call; silently skip if this is not a PR
|
||||
PR_INFO=$(gh pr view "${PR_NUMBER}" --json isCrossRepository,headRepositoryOwner,headRepository 2>/dev/null || echo "")
|
||||
if [ -z "$PR_INFO" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
IS_FORK=$(echo "$PR_INFO" | jq -r '.isCrossRepository')
|
||||
if [ "$IS_FORK" = "true" ]; then
|
||||
FORK_OWNER=$(echo "$PR_INFO" | jq -r '.headRepositoryOwner.login')
|
||||
FORK_REPO=$(echo "$PR_INFO" | jq -r '.headRepository.name')
|
||||
echo "Fork PR detected from ${FORK_OWNER}/${FORK_REPO}: updating origin to fork URL"
|
||||
git remote set-url origin "https://github.com/${FORK_OWNER}/${FORK_REPO}.git"
|
||||
fi
|
||||
|
||||
- name: Run Claude Code
|
||||
|
||||
Vendored
+4
-4
@@ -11,14 +11,14 @@ permissions:
|
||||
pull-requests: write
|
||||
discussions: write
|
||||
|
||||
concurrency:
|
||||
group: lock-threads
|
||||
|
||||
jobs:
|
||||
lock:
|
||||
if: github.repository == 'netbox-community/netbox'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@1bf7ec25051fe7c00bdd17e6a7cf3d7bfb7dc771 # v5.0.1
|
||||
- uses: dessant/lock-threads@v6.0.0
|
||||
with:
|
||||
issue-inactive-days: 90
|
||||
pr-inactive-days: 30
|
||||
discussion-inactive-days: 180
|
||||
issue-lock-reason: 'resolved'
|
||||
|
||||
@@ -215,6 +215,7 @@ if obj.pk and hasattr(obj, 'snapshot'):
|
||||
obj.snapshot()
|
||||
|
||||
obj.property = "New Value"
|
||||
obj._changelog_message = 'Example Message Text' # Optional
|
||||
obj.full_clean()
|
||||
obj.save()
|
||||
```
|
||||
|
||||
@@ -36,13 +36,16 @@ If false, synchronization will be disabled.
|
||||
|
||||
### Ignore Rules
|
||||
|
||||
A set of rules (one per line) identifying filenames to ignore during synchronization. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
|
||||
A set of rules (one per line) identifying files or paths to ignore during synchronization. Rules are matched against both the full relative path (e.g. `subdir/file.txt`) and the bare filename, so path-based patterns can be used to exclude entire directories. Some examples are provided below. See Python's [`fnmatch()` documentation](https://docs.python.org/3/library/fnmatch.html) for a complete reference.
|
||||
|
||||
| Rule | Description |
|
||||
|----------------|------------------------------------------|
|
||||
| `README` | Ignore any files named `README` |
|
||||
| `*.txt` | Ignore any files with a `.txt` extension |
|
||||
| `data???.json` | Ignore e.g. `data123.json` |
|
||||
| Rule | Description |
|
||||
|-----------------------|------------------------------------------------------|
|
||||
| `README` | Ignore any files named `README` |
|
||||
| `*.txt` | Ignore any files with a `.txt` extension |
|
||||
| `data???.json` | Ignore e.g. `data123.json` |
|
||||
| `subdir/*` | Ignore all files within `subdir/` |
|
||||
| `subdir/*/*` | Ignore all files one level deep within `subdir/` |
|
||||
| `*/dev/*` | Ignore files inside any directory named `dev/` |
|
||||
|
||||
### Sync Interval
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ class DataSourceForm(PrimaryModelForm):
|
||||
attrs={
|
||||
'rows': 5,
|
||||
'class': 'font-monospace',
|
||||
'placeholder': '.cache\n*.txt'
|
||||
'placeholder': '.cache\n*.txt\nsubdir/*'
|
||||
}
|
||||
),
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
ignore_rules = models.TextField(
|
||||
verbose_name=_('ignore rules'),
|
||||
blank=True,
|
||||
help_text=_("Patterns (one per line) matching files to ignore when syncing")
|
||||
help_text=_("Patterns (one per line) matching files or paths to ignore when syncing")
|
||||
)
|
||||
parameters = models.JSONField(
|
||||
verbose_name=_('parameters'),
|
||||
@@ -258,21 +258,22 @@ class DataSource(JobsMixin, PrimaryModel):
|
||||
if path.startswith('.'):
|
||||
continue
|
||||
for file_name in file_names:
|
||||
if not self._ignore(file_name):
|
||||
paths.add(os.path.join(path, file_name))
|
||||
file_path = os.path.join(path, file_name)
|
||||
if not self._ignore(file_path):
|
||||
paths.add(file_path)
|
||||
|
||||
logger.debug(f"Found {len(paths)} files")
|
||||
return paths
|
||||
|
||||
def _ignore(self, filename):
|
||||
def _ignore(self, file_path):
|
||||
"""
|
||||
Returns a boolean indicating whether the file should be ignored per the DataSource's configured
|
||||
ignore rules.
|
||||
ignore rules. file_path is the full relative path (e.g. "subdir/file.txt").
|
||||
"""
|
||||
if filename.startswith('.'):
|
||||
if os.path.basename(file_path).startswith('.'):
|
||||
return True
|
||||
for rule in self.ignore_rules.splitlines():
|
||||
if fnmatchcase(filename, rule):
|
||||
if fnmatchcase(file_path, rule) or fnmatchcase(os.path.basename(file_path), rule):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
@@ -10,6 +10,26 @@ from dcim.models import Device, Location, Site
|
||||
from netbox.constants import CENSOR_TOKEN, CENSOR_TOKEN_CHANGED
|
||||
|
||||
|
||||
class DataSourceIgnoreRulesTestCase(TestCase):
|
||||
|
||||
def test_no_ignore_rules(self):
|
||||
ds = DataSource(ignore_rules='')
|
||||
self.assertFalse(ds._ignore('README.md'))
|
||||
self.assertFalse(ds._ignore('subdir/file.py'))
|
||||
|
||||
def test_ignore_by_filename(self):
|
||||
ds = DataSource(ignore_rules='*.txt')
|
||||
self.assertTrue(ds._ignore('notes.txt'))
|
||||
self.assertTrue(ds._ignore('subdir/notes.txt'))
|
||||
self.assertFalse(ds._ignore('notes.py'))
|
||||
|
||||
def test_ignore_by_subdirectory(self):
|
||||
ds = DataSource(ignore_rules='dev/*')
|
||||
self.assertTrue(ds._ignore('dev/README.md'))
|
||||
self.assertTrue(ds._ignore('dev/script.py'))
|
||||
self.assertFalse(ds._ignore('prod/script.py'))
|
||||
|
||||
|
||||
class DataSourceChangeLoggingTestCase(TestCase):
|
||||
|
||||
def test_password_added_on_create(self):
|
||||
|
||||
@@ -121,13 +121,52 @@ class ScopedImportForm(forms.Form):
|
||||
required=False,
|
||||
label=_('Scope type (app & model)')
|
||||
)
|
||||
scope_name = forms.CharField(
|
||||
required=False,
|
||||
label=_('Scope name'),
|
||||
help_text=_('Name of the assigned scope object (if not using ID)')
|
||||
)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
|
||||
scope_id = self.cleaned_data.get('scope_id')
|
||||
scope_name = self.cleaned_data.get('scope_name')
|
||||
scope_type = self.cleaned_data.get('scope_type')
|
||||
if scope_type and not scope_id:
|
||||
|
||||
# Cannot specify both scope_name and scope_id
|
||||
if scope_name and scope_id:
|
||||
raise ValidationError(_("scope_name and scope_id are mutually exclusive."))
|
||||
|
||||
# Must specify scope_type with scope_name or scope_id
|
||||
if scope_name and not scope_type:
|
||||
raise ValidationError(_("scope_type must be specified when using scope_name"))
|
||||
if scope_id and not scope_type:
|
||||
raise ValidationError(_("scope_type must be specified when using scope_id"))
|
||||
|
||||
# Look up the scope object by name
|
||||
if scope_type and scope_name:
|
||||
model = scope_type.model_class()
|
||||
try:
|
||||
scope_obj = model.objects.get(name=scope_name)
|
||||
except model.DoesNotExist:
|
||||
raise ValidationError({
|
||||
'scope_name': _('{scope_type} "{name}" not found.').format(
|
||||
scope_type=bettertitle(model._meta.verbose_name),
|
||||
name=scope_name
|
||||
)
|
||||
})
|
||||
except model.MultipleObjectsReturned:
|
||||
raise ValidationError({
|
||||
'scope_name': _(
|
||||
'Multiple {scope_type} objects match "{name}". Use scope_id to specify the intended object.'
|
||||
).format(
|
||||
scope_type=bettertitle(model._meta.verbose_name),
|
||||
name=scope_name,
|
||||
)
|
||||
})
|
||||
self.cleaned_data['scope_id'] = scope_obj.pk
|
||||
elif scope_type and not scope_id:
|
||||
raise ValidationError({
|
||||
'scope_id': _(
|
||||
"Please select a {scope_type}."
|
||||
|
||||
@@ -293,7 +293,6 @@ class Cable(PrimaryModel):
|
||||
self._pk = self.pk
|
||||
|
||||
if self._orig_profile != self.profile:
|
||||
print(f'profile changed from {self._orig_profile} to {self.profile}')
|
||||
self.update_terminations(force=True)
|
||||
elif self._terminations_modified:
|
||||
self.update_terminations()
|
||||
@@ -403,6 +402,15 @@ class Cable(PrimaryModel):
|
||||
"""
|
||||
a_terminations, b_terminations = self.get_terminations()
|
||||
|
||||
# When force-recreating terminations (e.g. after a profile change), cache the termination objects
|
||||
# from the database before deleting, so they are available for recreation. Without this, the
|
||||
# a_terminations/b_terminations properties would query the DB after deletion and return empty lists.
|
||||
if force:
|
||||
if not hasattr(self, '_a_terminations'):
|
||||
self._a_terminations = list(a_terminations.keys())
|
||||
if not hasattr(self, '_b_terminations'):
|
||||
self._b_terminations = list(b_terminations.keys())
|
||||
|
||||
# Delete any stale CableTerminations
|
||||
for termination, ct in a_terminations.items():
|
||||
if force or (termination.pk and termination not in self.a_terminations):
|
||||
|
||||
@@ -1205,7 +1205,8 @@ class MACAddressTable(PrimaryModelTable):
|
||||
verbose_name=_('Parent')
|
||||
)
|
||||
is_primary = columns.BooleanColumn(
|
||||
verbose_name=_('Primary')
|
||||
verbose_name=_('Primary'),
|
||||
orderable=False,
|
||||
)
|
||||
tags = columns.TagColumn(
|
||||
url_name='dcim:macaddress_list'
|
||||
|
||||
@@ -218,7 +218,7 @@ class RackReservationTable(TenancyColumnsMixin, PrimaryModelTable):
|
||||
class Meta(PrimaryModelTable.Meta):
|
||||
model = RackReservation
|
||||
fields = (
|
||||
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'created', 'tenant',
|
||||
'pk', 'id', 'reservation', 'site', 'location', 'rack', 'unit_list', 'status', 'user', 'tenant',
|
||||
'tenant_group', 'description', 'comments', 'tags', 'actions', 'created', 'last_updated',
|
||||
)
|
||||
default_columns = ('pk', 'reservation', 'site', 'rack', 'unit_list', 'status', 'user', 'description')
|
||||
|
||||
@@ -1201,6 +1201,35 @@ class CableTestCase(TestCase):
|
||||
with self.assertRaises(ValidationError):
|
||||
cable.clean()
|
||||
|
||||
def test_cable_profile_change_preserves_terminations(self):
|
||||
"""
|
||||
When a Cable's profile is changed via save() without explicitly setting terminations (as happens during
|
||||
bulk edit), the existing termination points must be preserved.
|
||||
"""
|
||||
cable = Cable.objects.first()
|
||||
interface1 = Interface.objects.get(device__name='TestDevice1', name='eth0')
|
||||
interface2 = Interface.objects.get(device__name='TestDevice2', name='eth0')
|
||||
|
||||
# Verify initial state: cable has terminations and no profile
|
||||
self.assertEqual(cable.profile, '')
|
||||
self.assertEqual(CableTermination.objects.filter(cable=cable).count(), 2)
|
||||
|
||||
# Simulate what bulk edit does: load the cable from DB, set profile via setattr, and save.
|
||||
# Crucially, do NOT set a_terminations or b_terminations on the instance.
|
||||
cable_from_db = Cable.objects.get(pk=cable.pk)
|
||||
cable_from_db.profile = CableProfileChoices.SINGLE_1C1P
|
||||
cable_from_db.save()
|
||||
|
||||
# Verify terminations are preserved
|
||||
self.assertEqual(CableTermination.objects.filter(cable=cable).count(), 2)
|
||||
|
||||
# Verify the correct interfaces are still terminated
|
||||
cable_from_db.refresh_from_db()
|
||||
a_terms = [ct.termination for ct in CableTermination.objects.filter(cable=cable, cable_end='A')]
|
||||
b_terms = [ct.termination for ct in CableTermination.objects.filter(cable=cable, cable_end='B')]
|
||||
self.assertEqual(a_terms, [interface1])
|
||||
self.assertEqual(b_terms, [interface2])
|
||||
|
||||
|
||||
class VirtualDeviceContextTestCase(TestCase):
|
||||
|
||||
|
||||
@@ -210,8 +210,8 @@ class PrefixImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
class Meta:
|
||||
model = Prefix
|
||||
fields = (
|
||||
'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_id',
|
||||
'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
'prefix', 'vrf', 'tenant', 'vlan_group', 'vlan_site', 'vlan', 'status', 'role', 'scope_type', 'scope_name',
|
||||
'scope_id', 'is_pool', 'mark_utilized', 'description', 'owner', 'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': _('Scope ID'),
|
||||
@@ -424,19 +424,36 @@ class IPAddressImportForm(PrimaryModelImportForm):
|
||||
# Set as primary for device/VM
|
||||
if self.cleaned_data.get('is_primary') is not None:
|
||||
parent = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
|
||||
parent.snapshot()
|
||||
if self.instance.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress if self.cleaned_data.get('is_primary') else None
|
||||
elif self.instance.address.version == 6:
|
||||
parent.primary_ip6 = ipaddress if self.cleaned_data.get('is_primary') else None
|
||||
parent.save()
|
||||
if self.cleaned_data.get('is_primary'):
|
||||
parent.snapshot()
|
||||
if self.instance.address.version == 4:
|
||||
parent.primary_ip4 = ipaddress
|
||||
elif self.instance.address.version == 6:
|
||||
parent.primary_ip6 = ipaddress
|
||||
parent.save()
|
||||
else:
|
||||
# Only clear the primary IP if this IP is currently set as primary
|
||||
if self.instance.address.version == 4 and parent.primary_ip4 == ipaddress:
|
||||
parent.snapshot()
|
||||
parent.primary_ip4 = None
|
||||
parent.save()
|
||||
elif self.instance.address.version == 6 and parent.primary_ip6 == ipaddress:
|
||||
parent.snapshot()
|
||||
parent.primary_ip6 = None
|
||||
parent.save()
|
||||
|
||||
# Set as OOB for device
|
||||
if self.cleaned_data.get('is_oob') is not None:
|
||||
parent = self.cleaned_data.get('device')
|
||||
parent.snapshot()
|
||||
parent.oob_ip = ipaddress if self.cleaned_data.get('is_oob') else None
|
||||
parent.save()
|
||||
if self.cleaned_data.get('is_oob'):
|
||||
parent.snapshot()
|
||||
parent.oob_ip = ipaddress
|
||||
parent.save()
|
||||
elif parent.oob_ip == ipaddress:
|
||||
# Only clear OOB if this IP is currently set as the OOB IP
|
||||
parent.snapshot()
|
||||
parent.oob_ip = None
|
||||
parent.save()
|
||||
|
||||
return ipaddress
|
||||
|
||||
@@ -457,7 +474,8 @@ class FHRPGroupImportForm(PrimaryModelImportForm):
|
||||
fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'name', 'description', 'owner', 'comments', 'tags')
|
||||
|
||||
|
||||
class VLANGroupImportForm(OrganizationalModelImportForm):
|
||||
class VLANGroupImportForm(ScopedImportForm, OrganizationalModelImportForm):
|
||||
# Override ScopedImportForm.scope_type to set custom queryset
|
||||
scope_type = CSVContentTypeField(
|
||||
queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES),
|
||||
required=False,
|
||||
@@ -477,10 +495,11 @@ class VLANGroupImportForm(OrganizationalModelImportForm):
|
||||
class Meta:
|
||||
model = VLANGroup
|
||||
fields = (
|
||||
'name', 'slug', 'scope_type', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner', 'comments', 'tags'
|
||||
'name', 'slug', 'scope_type', 'scope_name', 'scope_id', 'vid_ranges', 'tenant', 'description', 'owner',
|
||||
'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': 'Scope ID',
|
||||
'scope_id': _('Scope ID'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -364,7 +364,6 @@ class VLANTranslationPolicy(PrimaryModel):
|
||||
max_length=100,
|
||||
unique=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('VLAN translation policy')
|
||||
verbose_name_plural = _('VLAN translation policies')
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.test import TestCase
|
||||
|
||||
from dcim.models import Location, Region, Site, SiteGroup
|
||||
from dcim.constants import InterfaceTypeChoices
|
||||
from dcim.models import Device, DeviceRole, DeviceType, Interface, Location, Manufacturer, Region, Site, SiteGroup
|
||||
from ipam.forms import PrefixForm
|
||||
from ipam.forms.bulk_import import IPAddressImportForm
|
||||
|
||||
|
||||
class PrefixFormTestCase(TestCase):
|
||||
@@ -41,3 +43,56 @@ class PrefixFormTestCase(TestCase):
|
||||
})
|
||||
|
||||
assert 'data-dynamic-params' not in form.fields['vlan'].widget.attrs
|
||||
|
||||
|
||||
class IPAddressImportFormTestCase(TestCase):
|
||||
"""Tests for IPAddressImportForm bulk import behavior."""
|
||||
|
||||
@classmethod
|
||||
def setUpTestData(cls):
|
||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||
manufacturer = Manufacturer.objects.create(name='Manufacturer 1', slug='manufacturer-1')
|
||||
device_type = DeviceType.objects.create(manufacturer=manufacturer, model='Model 1', slug='model-1')
|
||||
device_role = DeviceRole.objects.create(name='Device Role 1', slug='device-role-1')
|
||||
cls.device = Device.objects.create(
|
||||
name='Device 1',
|
||||
site=site,
|
||||
device_type=device_type,
|
||||
role=device_role,
|
||||
)
|
||||
cls.interface = Interface.objects.create(
|
||||
device=cls.device,
|
||||
name='eth0',
|
||||
type=InterfaceTypeChoices.TYPE_1GE_FIXED,
|
||||
)
|
||||
|
||||
def test_oob_import_not_cleared_by_subsequent_non_oob_row(self):
|
||||
"""
|
||||
Regression test for #21440: importing a second IP with is_oob=False should
|
||||
not clear the OOB IP set by a previous row with is_oob=True.
|
||||
"""
|
||||
form1 = IPAddressImportForm(data={
|
||||
'address': '10.10.10.1/24',
|
||||
'status': 'active',
|
||||
'device': 'Device 1',
|
||||
'interface': 'eth0',
|
||||
'is_oob': True,
|
||||
})
|
||||
self.assertTrue(form1.is_valid(), form1.errors)
|
||||
ip1 = form1.save()
|
||||
|
||||
self.device.refresh_from_db()
|
||||
self.assertEqual(self.device.oob_ip, ip1)
|
||||
|
||||
form2 = IPAddressImportForm(data={
|
||||
'address': '2001:db8::1/64',
|
||||
'status': 'active',
|
||||
'device': 'Device 1',
|
||||
'interface': 'eth0',
|
||||
'is_oob': False,
|
||||
})
|
||||
self.assertTrue(form2.is_valid(), form2.errors)
|
||||
form2.save()
|
||||
|
||||
self.device.refresh_from_db()
|
||||
self.assertEqual(self.device.oob_ip, ip1, "OOB IP was incorrectly cleared by a row with is_oob=False")
|
||||
|
||||
@@ -435,13 +435,21 @@ class PrefixTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
site = sites[0].pk
|
||||
cls.csv_data = (
|
||||
"vrf,prefix,status,scope_type,scope_id",
|
||||
f"VRF 1,10.4.0.0/16,active,dcim.site,{site}",
|
||||
f"VRF 1,10.5.0.0/16,active,dcim.site,{site}",
|
||||
f"VRF 1,10.6.0.0/16,active,dcim.site,{site}",
|
||||
)
|
||||
site = sites[0]
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"vrf,prefix,status,scope_type,scope_id",
|
||||
f"VRF 1,10.4.0.0/16,active,dcim.site,{site.pk}",
|
||||
f"VRF 1,10.5.0.0/16,active,dcim.site,{site.pk}",
|
||||
f"VRF 1,10.6.0.0/16,active,dcim.site,{site.pk}",
|
||||
),
|
||||
'scope_name': (
|
||||
"vrf,prefix,status,scope_type,scope_name",
|
||||
f"VRF 1,10.4.0.0/16,active,dcim.site,{site.name}",
|
||||
f"VRF 1,10.5.0.0/16,active,dcim.site,{site.name}",
|
||||
f"VRF 1,10.6.0.0/16,active,dcim.site,{site.name}",
|
||||
),
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,description,status",
|
||||
@@ -532,6 +540,32 @@ scope_id: {site.pk}
|
||||
self.assertEqual(prefix.vlan.vid, 101)
|
||||
self.assertEqual(prefix.scope, site)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_prefix_import_with_scope_name(self):
|
||||
"""
|
||||
Test YAML-based import using scope_name instead of scope_id.
|
||||
"""
|
||||
site = Site.objects.get(name='Site 1')
|
||||
IMPORT_DATA = """
|
||||
prefix: 10.1.3.0/24
|
||||
status: active
|
||||
scope_type: dcim.site
|
||||
scope_name: Site 1
|
||||
"""
|
||||
# Add all required permissions to the test user
|
||||
self.add_permissions('ipam.view_prefix', 'ipam.add_prefix')
|
||||
|
||||
form_data = {
|
||||
'data': IMPORT_DATA,
|
||||
'format': 'yaml'
|
||||
}
|
||||
response = self.client.post(reverse('ipam:prefix_bulk_import'), data=form_data, follow=True)
|
||||
self.assertHttpStatus(response, 200)
|
||||
|
||||
prefix = Prefix.objects.get(prefix='10.1.3.0/24')
|
||||
self.assertEqual(prefix.status, PrefixStatusChoices.STATUS_ACTIVE)
|
||||
self.assertEqual(prefix.scope, site)
|
||||
|
||||
@override_settings(EXEMPT_VIEW_PERMISSIONS=['*'])
|
||||
def test_prefix_import_with_vlan_group(self):
|
||||
"""
|
||||
@@ -884,12 +918,20 @@ class VLANGroupTestCase(ViewTestCases.OrganizationalObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,slug,scope_type,scope_id,description",
|
||||
"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
|
||||
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
|
||||
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
|
||||
)
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"name,slug,scope_type,scope_id,description",
|
||||
"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
|
||||
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].pk},Fifth VLAN group",
|
||||
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].pk},Sixth VLAN group",
|
||||
),
|
||||
'scope_name': (
|
||||
"name,slug,scope_type,scope_name,description",
|
||||
"VLAN Group 4,vlan-group-4,,,Fourth VLAN group",
|
||||
f"VLAN Group 5,vlan-group-5,dcim.site,{sites[0].name},Fifth VLAN group",
|
||||
f"VLAN Group 6,vlan-group-6,dcim.site,{sites[1].name},Sixth VLAN group",
|
||||
),
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,description",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
from netbox.ui import attrs
|
||||
|
||||
|
||||
class VRFDisplayAttr(attrs.ObjectAttribute):
|
||||
"""
|
||||
Renders a VRF reference, displaying 'Global' when no VRF is assigned.
|
||||
"""
|
||||
template_name = 'ipam/attrs/vrf.html'
|
||||
|
||||
def render(self, obj, context):
|
||||
value = self.get_value(obj)
|
||||
return render_to_string(self.template_name, {
|
||||
**self.get_context(obj, context),
|
||||
'name': context.get('name', ''),
|
||||
'value': value,
|
||||
})
|
||||
|
||||
|
||||
class VRFDisplayWithRDAttr(VRFDisplayAttr):
|
||||
"""
|
||||
Renders a VRF reference with its route distinguisher.
|
||||
"""
|
||||
template_name = 'ipam/attrs/vrf_with_rd.html'
|
||||
+202
-2
@@ -2,14 +2,15 @@ from django.contrib.contenttypes.models import ContentType
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import actions, panels
|
||||
from netbox.ui import actions, attrs, panels
|
||||
|
||||
from .attrs import VRFDisplayAttr, VRFDisplayWithRDAttr
|
||||
|
||||
|
||||
class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
|
||||
"""
|
||||
A panel which lists all FHRP group assignments for a given object.
|
||||
"""
|
||||
|
||||
template_name = 'ipam/panels/fhrp_groups.html'
|
||||
title = _('FHRP Groups')
|
||||
actions = [
|
||||
@@ -35,3 +36,202 @@ class FHRPGroupAssignmentsPanel(panels.ObjectPanel):
|
||||
label=_('Assign Group'),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class VRFPanel(panels.ObjectAttributesPanel):
|
||||
rd = attrs.TextAttr('rd', label=_('Route Distinguisher'), style='font-monospace')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
enforce_unique = attrs.BooleanAttr('enforce_unique', label=_('Unique IP Space'))
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class RouteTargetPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name', style='font-monospace')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class RIRPanel(panels.OrganizationalObjectPanel):
|
||||
is_private = attrs.BooleanAttr('is_private', label=_('Private'))
|
||||
|
||||
|
||||
class ASNRangePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
|
||||
range = attrs.TextAttr('range_as_string_with_asdot', label=_('Range'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class ASNPanel(panels.ObjectAttributesPanel):
|
||||
asn = attrs.TextAttr('asn_with_asdot', label=_('AS Number'))
|
||||
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class AggregatePanel(panels.ObjectAttributesPanel):
|
||||
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
|
||||
rir = attrs.RelatedObjectAttr('rir', linkify=True, label=_('RIR'))
|
||||
utilization = attrs.TemplatedAttr(
|
||||
'prefix',
|
||||
template_name='ipam/aggregate/attrs/utilization.html',
|
||||
label=_('Utilization'),
|
||||
)
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
date_added = attrs.DateTimeAttr('date_added', spec='date', label=_('Date Added'))
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class RolePanel(panels.OrganizationalObjectPanel):
|
||||
weight = attrs.NumericAttr('weight')
|
||||
|
||||
|
||||
class IPRangePanel(panels.ObjectAttributesPanel):
|
||||
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
|
||||
start_address = attrs.TextAttr('start_address', label=_('Starting Address'))
|
||||
end_address = attrs.TextAttr('end_address', label=_('Ending Address'))
|
||||
size = attrs.NumericAttr('size')
|
||||
mark_populated = attrs.BooleanAttr('mark_populated', label=_('Marked Populated'))
|
||||
mark_utilized = attrs.BooleanAttr('mark_utilized', label=_('Marked Utilized'))
|
||||
utilization = attrs.TemplatedAttr(
|
||||
'utilization',
|
||||
template_name='ipam/iprange/attrs/utilization.html',
|
||||
label=_('Utilization'),
|
||||
)
|
||||
vrf = VRFDisplayWithRDAttr('vrf', label=_('VRF'))
|
||||
role = attrs.RelatedObjectAttr('role', linkify=True)
|
||||
status = attrs.ChoiceAttr('status')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class IPAddressPanel(panels.ObjectAttributesPanel):
|
||||
family = attrs.TextAttr('family', format_string='IPv{}', label=_('Family'))
|
||||
vrf = VRFDisplayAttr('vrf', label=_('VRF'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
role = attrs.ChoiceAttr('role')
|
||||
dns_name = attrs.TextAttr('dns_name', label=_('DNS Name'))
|
||||
description = attrs.TextAttr('description')
|
||||
assigned_object = attrs.TemplatedAttr(
|
||||
'assigned_object',
|
||||
template_name='ipam/ipaddress/attrs/assigned_object.html',
|
||||
label=_('Assignment'),
|
||||
)
|
||||
nat_inside = attrs.TemplatedAttr(
|
||||
'nat_inside',
|
||||
template_name='ipam/ipaddress/attrs/nat_inside.html',
|
||||
label=_('NAT (inside)'),
|
||||
)
|
||||
nat_outside = attrs.TemplatedAttr(
|
||||
'nat_outside',
|
||||
template_name='ipam/ipaddress/attrs/nat_outside.html',
|
||||
label=_('NAT (outside)'),
|
||||
)
|
||||
is_primary_ip = attrs.BooleanAttr('is_primary_ip', label=_('Primary IP'))
|
||||
is_oob_ip = attrs.BooleanAttr('is_oob_ip', label=_('OOB IP'))
|
||||
|
||||
|
||||
class VLANGroupPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
|
||||
vid_ranges = attrs.TemplatedAttr(
|
||||
'vid_ranges_items',
|
||||
template_name='ipam/vlangroup/attrs/vid_ranges.html',
|
||||
label=_('VLAN IDs'),
|
||||
)
|
||||
utilization = attrs.UtilizationAttr('utilization')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
|
||||
|
||||
class VLANTranslationPolicyPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class VLANTranslationRulePanel(panels.ObjectAttributesPanel):
|
||||
policy = attrs.RelatedObjectAttr('policy', linkify=True)
|
||||
local_vid = attrs.NumericAttr('local_vid', label=_('Local VID'))
|
||||
remote_vid = attrs.NumericAttr('remote_vid', label=_('Remote VID'))
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class FHRPGroupPanel(panels.ObjectAttributesPanel):
|
||||
protocol = attrs.ChoiceAttr('protocol')
|
||||
group_id = attrs.NumericAttr('group_id', label=_('Group ID'))
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
member_count = attrs.NumericAttr('member_count', label=_('Members'))
|
||||
|
||||
|
||||
class FHRPGroupAuthPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Authentication')
|
||||
|
||||
auth_type = attrs.ChoiceAttr('auth_type', label=_('Authentication Type'))
|
||||
auth_key = attrs.TextAttr('auth_key', label=_('Authentication Key'))
|
||||
|
||||
|
||||
class VLANPanel(panels.ObjectAttributesPanel):
|
||||
region = attrs.NestedObjectAttr('site.region', linkify=True, label=_('Region'))
|
||||
site = attrs.RelatedObjectAttr('site', linkify=True)
|
||||
group = attrs.RelatedObjectAttr('group', linkify=True)
|
||||
vid = attrs.NumericAttr('vid', label=_('VLAN ID'))
|
||||
name = attrs.TextAttr('name')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
role = attrs.RelatedObjectAttr('role', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
qinq_role = attrs.ChoiceAttr('qinq_role', label=_('Q-in-Q Role'))
|
||||
qinq_svlan = attrs.RelatedObjectAttr('qinq_svlan', linkify=True, label=_('Q-in-Q SVLAN'))
|
||||
l2vpn = attrs.RelatedObjectAttr('l2vpn_termination.l2vpn', linkify=True, label=_('L2VPN'))
|
||||
|
||||
|
||||
class VLANCustomerVLANsPanel(panels.ObjectsTablePanel):
|
||||
"""
|
||||
A panel listing customer VLANs (C-VLANs) for an S-VLAN. Only renders when the VLAN has Q-in-Q
|
||||
role 'svlan'.
|
||||
"""
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
'ipam.vlan',
|
||||
filters={'qinq_svlan_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Customer VLANs'),
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.vlan',
|
||||
url_params={
|
||||
'qinq_role': 'cvlan',
|
||||
'qinq_svlan': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
label=_('Add a VLAN'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def render(self, context):
|
||||
obj = context.get('object')
|
||||
if not obj or obj.qinq_role != 'svlan':
|
||||
return ''
|
||||
return super().render(context)
|
||||
|
||||
|
||||
class ServiceTemplatePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
protocol = attrs.ChoiceAttr('protocol')
|
||||
ports = attrs.TextAttr('port_list', label=_('Ports'))
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
|
||||
class ServicePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
parent = attrs.RelatedObjectAttr('parent', linkify=True)
|
||||
protocol = attrs.ChoiceAttr('protocol')
|
||||
ports = attrs.TextAttr('port_list', label=_('Ports'))
|
||||
ip_addresses = attrs.TemplatedAttr(
|
||||
'ipaddresses',
|
||||
template_name='ipam/service/attrs/ip_addresses.html',
|
||||
label=_('IP Addresses'),
|
||||
)
|
||||
description = attrs.TextAttr('description')
|
||||
|
||||
+306
-36
@@ -9,8 +9,16 @@ from circuits.models import Provider
|
||||
from dcim.filtersets import InterfaceFilterSet
|
||||
from dcim.forms import InterfaceFilterForm
|
||||
from dcim.models import Device, Interface, Site
|
||||
from ipam.tables import VLANTranslationRuleTable
|
||||
from extras.ui.panels import CustomFieldsPanel, TagsPanel
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
CommentsPanel,
|
||||
ContextTablePanel,
|
||||
ObjectsTablePanel,
|
||||
RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.tables import get_table_ordering
|
||||
@@ -23,6 +31,7 @@ from . import filtersets, forms, tables
|
||||
from .choices import PrefixStatusChoices
|
||||
from .constants import *
|
||||
from .models import *
|
||||
from .ui import panels
|
||||
from .utils import add_available_vlans, add_requested_prefixes, annotate_ip_space
|
||||
|
||||
#
|
||||
@@ -41,6 +50,27 @@ class VRFListView(generic.ObjectListView):
|
||||
@register_model_view(VRF)
|
||||
class VRFView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VRF.objects.all()
|
||||
layout = layout.Layout(
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
panels.VRFPanel(),
|
||||
TagsPanel(),
|
||||
),
|
||||
layout.Column(
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
ContextTablePanel('import_targets_table', title=_('Import Route Targets')),
|
||||
),
|
||||
layout.Column(
|
||||
ContextTablePanel('export_targets_table', title=_('Export Route Targets')),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
import_targets_table = tables.RouteTargetTable(
|
||||
@@ -134,6 +164,50 @@ class RouteTargetListView(generic.ObjectListView):
|
||||
@register_model_view(RouteTarget)
|
||||
class RouteTargetView(generic.ObjectView):
|
||||
queryset = RouteTarget.objects.all()
|
||||
layout = layout.Layout(
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
panels.RouteTargetPanel(),
|
||||
TagsPanel(),
|
||||
),
|
||||
layout.Column(
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
ObjectsTablePanel(
|
||||
'ipam.vrf',
|
||||
filters={'import_target_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Importing VRFs'),
|
||||
),
|
||||
),
|
||||
layout.Column(
|
||||
ObjectsTablePanel(
|
||||
'ipam.vrf',
|
||||
filters={'export_target_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Exporting VRFs'),
|
||||
),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
ObjectsTablePanel(
|
||||
'vpn.l2vpn',
|
||||
filters={'import_target_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Importing L2VPNs'),
|
||||
),
|
||||
),
|
||||
layout.Column(
|
||||
ObjectsTablePanel(
|
||||
'vpn.l2vpn',
|
||||
filters={'export_target_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Exporting L2VPNs'),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(RouteTarget, 'add', detail=False)
|
||||
@@ -192,6 +266,17 @@ class RIRListView(generic.ObjectListView):
|
||||
@register_model_view(RIR)
|
||||
class RIRView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = RIR.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.RIRPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -257,6 +342,16 @@ class ASNRangeListView(generic.ObjectListView):
|
||||
@register_model_view(ASNRange)
|
||||
class ASNRangeView(generic.ObjectView):
|
||||
queryset = ASNRange.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ASNRangePanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(ASNRange, 'asns')
|
||||
@@ -337,6 +432,17 @@ class ASNListView(generic.ObjectListView):
|
||||
@register_model_view(ASN)
|
||||
class ASNView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = ASN.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ASNPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -412,6 +518,16 @@ class AggregateListView(generic.ObjectListView):
|
||||
@register_model_view(Aggregate)
|
||||
class AggregateView(generic.ObjectView):
|
||||
queryset = Aggregate.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.AggregatePanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(Aggregate, 'prefixes')
|
||||
@@ -506,6 +622,17 @@ class RoleListView(generic.ObjectListView):
|
||||
@register_model_view(Role)
|
||||
class RoleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = Role.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.RolePanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -569,6 +696,21 @@ class PrefixListView(generic.ObjectListView):
|
||||
@register_model_view(Prefix)
|
||||
class PrefixView(generic.ObjectView):
|
||||
queryset = Prefix.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
TemplatePanel('ipam/panels/prefix.html'),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel('ipam/panels/prefix_addressing.html'),
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ContextTablePanel('duplicate_prefix_table', title=_('Duplicate Prefixes')),
|
||||
ContextTablePanel('parent_prefix_table', title=_('Parent Prefixes')),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
try:
|
||||
@@ -608,11 +750,13 @@ class PrefixView(generic.ObjectView):
|
||||
)
|
||||
duplicate_prefix_table.configure(request)
|
||||
|
||||
return {
|
||||
result = {
|
||||
'aggregate': aggregate,
|
||||
'parent_prefix_table': parent_prefix_table,
|
||||
'duplicate_prefix_table': duplicate_prefix_table,
|
||||
}
|
||||
if duplicate_prefixes.exists():
|
||||
result['duplicate_prefix_table'] = duplicate_prefix_table
|
||||
return result
|
||||
|
||||
|
||||
@register_model_view(Prefix, 'prefixes')
|
||||
@@ -756,6 +900,19 @@ class IPRangeListView(generic.ObjectListView):
|
||||
@register_model_view(IPRange)
|
||||
class IPRangeView(generic.ObjectView):
|
||||
queryset = IPRange.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.IPRangePanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ContextTablePanel('parent_prefixes_table', title=_('Parent Prefixes')),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
|
||||
@@ -853,6 +1010,23 @@ class IPAddressListView(generic.ObjectListView):
|
||||
@register_model_view(IPAddress)
|
||||
class IPAddressView(generic.ObjectView):
|
||||
queryset = IPAddress.objects.prefetch_related('vrf__tenant', 'tenant')
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.IPAddressPanel(),
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
ContextTablePanel('parent_prefixes_table', title=_('Parent Prefixes')),
|
||||
ContextTablePanel('duplicate_ips_table', title=_('Duplicate IPs')),
|
||||
ObjectsTablePanel(
|
||||
'ipam.service',
|
||||
filters={'ip_address_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Application Services'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# Parent prefixes table
|
||||
@@ -885,10 +1059,12 @@ class IPAddressView(generic.ObjectView):
|
||||
duplicate_ips_table = tables.IPAddressTable(duplicate_ips[:10], orderable=False)
|
||||
duplicate_ips_table.configure(request)
|
||||
|
||||
return {
|
||||
result = {
|
||||
'parent_prefixes_table': parent_prefixes_table,
|
||||
'duplicate_ips_table': duplicate_ips_table,
|
||||
}
|
||||
if duplicate_ips.exists():
|
||||
result['duplicate_ips_table'] = duplicate_ips_table
|
||||
return result
|
||||
|
||||
|
||||
@register_model_view(IPAddress, 'add', detail=False)
|
||||
@@ -1038,6 +1214,17 @@ class VLANGroupListView(generic.ObjectListView):
|
||||
@register_model_view(VLANGroup)
|
||||
class VLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = VLANGroup.objects.annotate_utilization()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VLANGroupPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -1125,19 +1312,32 @@ class VLANTranslationPolicyListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationPolicy)
|
||||
class VLANTranslationPolicyView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
class VLANTranslationPolicyView(generic.ObjectView):
|
||||
queryset = VLANTranslationPolicy.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
vlan_translation_table = VLANTranslationRuleTable(
|
||||
data=instance.rules.all(),
|
||||
orderable=False
|
||||
)
|
||||
vlan_translation_table.configure(request)
|
||||
|
||||
return {
|
||||
'vlan_translation_table': vlan_translation_table,
|
||||
}
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VLANTranslationPolicyPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'ipam.vlantranslationrule',
|
||||
filters={'policy_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('VLAN Translation Rules'),
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.vlantranslationrule',
|
||||
url_params={'policy': lambda ctx: ctx['object'].pk},
|
||||
label=_('Add Rule'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationPolicy, 'add', detail=False)
|
||||
@@ -1193,13 +1393,17 @@ class VLANTranslationRuleListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationRule)
|
||||
class VLANTranslationRuleView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
class VLANTranslationRuleView(generic.ObjectView):
|
||||
queryset = VLANTranslationRule.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VLANTranslationRulePanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(VLANTranslationRule, 'add', detail=False)
|
||||
@@ -1251,7 +1455,36 @@ class FHRPGroupListView(generic.ObjectListView):
|
||||
|
||||
@register_model_view(FHRPGroup)
|
||||
class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = FHRPGroup.objects.all()
|
||||
queryset = FHRPGroup.objects.annotate(
|
||||
member_count=count_related(FHRPGroupAssignment, 'group')
|
||||
)
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.FHRPGroupPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.FHRPGroupAuthPanel(),
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'ipam.ipaddress',
|
||||
filters={'fhrpgroup_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Virtual IP Addresses'),
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.ipaddress',
|
||||
url_params={'fhrpgroup': lambda ctx: ctx['object'].pk},
|
||||
label=_('Add IP Address'),
|
||||
),
|
||||
],
|
||||
),
|
||||
ContextTablePanel('members_table', title=_('Members')),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
# Get assigned interfaces
|
||||
@@ -1276,7 +1509,6 @@ class FHRPGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
),
|
||||
),
|
||||
'members_table': members_table,
|
||||
'member_count': FHRPGroupAssignment.objects.filter(group=instance).count(),
|
||||
}
|
||||
|
||||
|
||||
@@ -1379,17 +1611,35 @@ class VLANListView(generic.ObjectListView):
|
||||
@register_model_view(VLAN)
|
||||
class VLANView(generic.ObjectView):
|
||||
queryset = VLAN.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
prefixes = Prefix.objects.restrict(request.user, 'view').filter(vlan=instance).prefetch_related(
|
||||
'vrf', 'scope', 'role', 'tenant'
|
||||
)
|
||||
prefix_table = tables.PrefixTable(list(prefixes), exclude=('vlan', 'utilization'), orderable=False)
|
||||
prefix_table.configure(request)
|
||||
|
||||
return {
|
||||
'prefix_table': prefix_table,
|
||||
}
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.VLANPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'ipam.prefix',
|
||||
filters={'vlan_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Prefixes'),
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'ipam.prefix',
|
||||
url_params={
|
||||
'tenant': lambda ctx: ctx['object'].tenant.pk if ctx['object'].tenant else None,
|
||||
'site': lambda ctx: ctx['object'].site.pk if ctx['object'].site else None,
|
||||
'vlan': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
label=_('Add a Prefix'),
|
||||
),
|
||||
],
|
||||
),
|
||||
panels.VLANCustomerVLANsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(VLAN, 'interfaces')
|
||||
@@ -1483,6 +1733,16 @@ class ServiceTemplateListView(generic.ObjectListView):
|
||||
@register_model_view(ServiceTemplate)
|
||||
class ServiceTemplateView(generic.ObjectView):
|
||||
queryset = ServiceTemplate.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ServiceTemplatePanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(ServiceTemplate, 'add', detail=False)
|
||||
@@ -1539,6 +1799,16 @@ class ServiceListView(generic.ObjectListView):
|
||||
@register_model_view(Service)
|
||||
class ServiceView(generic.ObjectView):
|
||||
queryset = Service.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.ServicePanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
context = {}
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+4
-4
File diff suppressed because one or more lines are too long
+4
-4
File diff suppressed because one or more lines are too long
@@ -20,12 +20,7 @@ function storeColorMode(mode: ColorMode): void {
|
||||
}
|
||||
|
||||
function updateElements(targetMode: ColorMode): void {
|
||||
const body = document.querySelector('body');
|
||||
if (body && targetMode == 'dark') {
|
||||
body.setAttribute('data-bs-theme', 'dark');
|
||||
} else if (body) {
|
||||
body.setAttribute('data-bs-theme', 'light');
|
||||
}
|
||||
document.documentElement.setAttribute('data-bs-theme', targetMode);
|
||||
|
||||
for (const elevation of getElements<HTMLObjectElement>('.rack_elevation')) {
|
||||
const svg = elevation.firstElementChild ?? null;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { RecursivePartial, TomOption, TomSettings, TomInput } from 'tom-select/dist/cjs/types';
|
||||
import { addClasses } from 'tom-select/src/vanilla.ts';
|
||||
import queryString from 'query-string';
|
||||
import TomSelect from 'tom-select';
|
||||
import type { Stringifiable } from 'query-string';
|
||||
import { DynamicParamsMap } from './dynamicParamsMap';
|
||||
import { NetBoxTomSelect } from './netboxTomSelect';
|
||||
|
||||
// Transitional
|
||||
import { QueryFilter, PathFilter } from '../types';
|
||||
import { getElement, replaceAll } from '../../util';
|
||||
|
||||
// Extends TomSelect to provide enhanced fetching of options via the REST API
|
||||
export class DynamicTomSelect extends TomSelect {
|
||||
// Extends NetBoxTomSelect to provide enhanced fetching of options via the REST API
|
||||
export class DynamicTomSelect extends NetBoxTomSelect {
|
||||
public readonly nullOption: Nullable<TomOption> = null;
|
||||
|
||||
// Transitional code from APISelect
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import TomSelect from 'tom-select';
|
||||
|
||||
/**
|
||||
* Extends TomSelect to work around a browser autofill bug where Edge's "last used" autofill
|
||||
* simultaneously focuses multiple inputs, triggering a cascading focus/open/blur loop between
|
||||
* TomSelect instances.
|
||||
*
|
||||
* Root cause: TomSelect's open() method calls focus(), which synchronously moves browser focus
|
||||
* to this instance's control input, then schedules setTimeout(onFocus, 0). When Edge autofill
|
||||
* has moved focus to a *different* select before the timeout fires, the delayed onFocus() call
|
||||
* re-steals browser focus back, causing the other instance to blur and close. Each instance's
|
||||
* deferred callback then repeats this, creating an infinite ping-pong loop.
|
||||
*
|
||||
* Fix: in the setTimeout callback, only proceed with onFocus() if this instance's element is
|
||||
* still the active element. If focus has already moved elsewhere, skip the call.
|
||||
*
|
||||
* Upstream bug: https://github.com/orchidjs/tom-select/issues/806
|
||||
* NetBox issue: https://github.com/netbox-community/netbox/issues/20077
|
||||
*/
|
||||
export class NetBoxTomSelect extends TomSelect {
|
||||
focus(): void {
|
||||
if (this.isDisabled || this.isReadOnly) return;
|
||||
|
||||
this.ignoreFocus = true;
|
||||
|
||||
const focusTarget = this.control_input.offsetWidth ? this.control_input : this.focus_node;
|
||||
focusTarget.focus();
|
||||
|
||||
setTimeout(() => {
|
||||
this.ignoreFocus = false;
|
||||
// Only proceed if this instance's element is still the active element. If Edge autofill
|
||||
// (or anything else) has moved focus to a different element in the interim, calling
|
||||
// onFocus() here would steal focus back and restart the cascade loop.
|
||||
if (document.activeElement === focusTarget || this.control.contains(document.activeElement)) {
|
||||
this.onFocus();
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TomOption } from 'tom-select/src/types';
|
||||
import TomSelect from 'tom-select';
|
||||
import { escape_html } from 'tom-select/src/utils';
|
||||
import { NetBoxTomSelect } from './classes/netboxTomSelect';
|
||||
import { getPlugins } from './config';
|
||||
import { getElements } from '../util';
|
||||
|
||||
@@ -9,7 +9,7 @@ export function initStaticSelects(): void {
|
||||
for (const select of getElements<HTMLSelectElement>(
|
||||
'select:not(.tomselected):not(.no-ts):not([size]):not(.api-select):not(.color-select)',
|
||||
)) {
|
||||
new TomSelect(select, {
|
||||
new NetBoxTomSelect(select, {
|
||||
...getPlugins(select),
|
||||
maxOptions: undefined,
|
||||
});
|
||||
@@ -25,7 +25,7 @@ export function initColorSelects(): void {
|
||||
}
|
||||
|
||||
for (const select of getElements<HTMLSelectElement>('select.color-select:not(.tomselected)')) {
|
||||
new TomSelect(select, {
|
||||
new NetBoxTomSelect(select, {
|
||||
...getPlugins(select),
|
||||
maxOptions: undefined,
|
||||
render: {
|
||||
|
||||
@@ -112,7 +112,7 @@ img.plugin-icon {
|
||||
}
|
||||
|
||||
|
||||
body[data-bs-theme=dark] {
|
||||
html[data-bs-theme=dark] {
|
||||
// Assuming icon is black/white line art, invert it and tone down brightness
|
||||
img.plugin-icon {
|
||||
filter: grayscale(100%) invert(100%) brightness(80%);
|
||||
|
||||
@@ -93,7 +93,7 @@ pre {
|
||||
}
|
||||
|
||||
// Dark mode overrides
|
||||
body[data-bs-theme=dark] {
|
||||
html[data-bs-theme=dark] {
|
||||
// Override background color alpha value
|
||||
::selection {
|
||||
background-color: rgba(var(--tblr-primary-rgb),.48);
|
||||
@@ -174,16 +174,11 @@ pre code {
|
||||
}
|
||||
|
||||
// Theme-based visibility utilities
|
||||
// Tabler's .hide-theme-* utilities expect data-bs-theme on :root, but NetBox applies
|
||||
// it to body. These overrides use higher specificity selectors to ensure theme-based
|
||||
// visibility works correctly. The :root:not(.dummy) pattern provides the additional
|
||||
// specificity needed to override Tabler's :root:not() rules.
|
||||
:root:not(.dummy) body[data-bs-theme='light'] .hide-theme-light,
|
||||
:root:not(.dummy) body[data-bs-theme='dark'] .hide-theme-dark {
|
||||
:root:not(.dummy)[data-bs-theme='light'] .hide-theme-light,
|
||||
:root:not(.dummy)[data-bs-theme='dark'] .hide-theme-dark {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:root:not(.dummy) body[data-bs-theme='dark'] .hide-theme-light,
|
||||
:root:not(.dummy) body[data-bs-theme='light'] .hide-theme-dark {
|
||||
:root:not(.dummy)[data-bs-theme='dark'] .hide-theme-light,
|
||||
:root:not(.dummy)[data-bs-theme='light'] .hide-theme-dark {
|
||||
display: inline-flex !important;
|
||||
}
|
||||
|
||||
@@ -77,13 +77,13 @@
|
||||
}
|
||||
|
||||
// Light theme styling
|
||||
body[data-bs-theme=light] .navbar-vertical.navbar-expand-lg {
|
||||
html[data-bs-theme=light] .navbar-vertical.navbar-expand-lg {
|
||||
// Background Gradient
|
||||
background: linear-gradient(180deg, rgba(0, 133, 125, 0.00) 0%, rgba(0, 133, 125, 0.10) 100%), #FFF;
|
||||
}
|
||||
|
||||
// Dark theme styling
|
||||
body[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg {
|
||||
html[data-bs-theme=dark] .navbar-vertical.navbar-expand-lg {
|
||||
|
||||
// Background Gradient
|
||||
background: linear-gradient(180deg, rgba(0, 242, 212, 0.00) 0%, rgba(0, 242, 212, 0.10) 100%), #001423;
|
||||
|
||||
@@ -59,7 +59,7 @@ table th.orderable a {
|
||||
color: var(--#{$prefix}body-color);
|
||||
}
|
||||
|
||||
body[data-bs-theme=dark] {
|
||||
html[data-bs-theme=dark] {
|
||||
// Adjust table header background color
|
||||
.table thead th, .markdown>table thead th {
|
||||
background: $rich-black !important;
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
{% endblock tabs %}
|
||||
|
||||
{% block controls %}
|
||||
{% add_button model %}
|
||||
{% if perms.core.add_managedfile and perms.extras.add_scriptmodule %}
|
||||
{% add_button model %}
|
||||
{% endif %}
|
||||
{% endblock controls %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -1,62 +1 @@
|
||||
{% extends 'ipam/aggregate/base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Aggregate" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "RIR" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:aggregate_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Utilization" %}</th>
|
||||
<td>
|
||||
{% utilization_graph object.get_utilization %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Date Added" %}</th>
|
||||
<td>{{ object.date_added|isodate|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{% utilization_graph object.get_utilization %}
|
||||
@@ -1,8 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
@@ -12,51 +8,3 @@
|
||||
<li class="breadcrumb-item"><a href="{% url 'ipam:asn_list' %}?range_id={{ object.range.pk }}">{{ object.range }}</a></li>
|
||||
{% endif %}
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "ASN" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "AS Number" %}</th>
|
||||
<td>{{ object.asn_with_asdot }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "RIR" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:asn_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,57 +1 @@
|
||||
{% extends 'ipam/asnrange/base.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "ASN Range" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "RIR" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:asnrange_list' %}?rir={{ object.rir.slug }}">{{ object.rir }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Range" %}</th>
|
||||
<td>{{ object.range_as_string_with_asdot }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers i18n %}
|
||||
{% if value %}{{ value|linkify }}{% else %}<span>{% trans "Global" %}</span>{% endif %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers i18n %}
|
||||
{% if value %}{{ value|linkify }} ({{ value.rd }}){% else %}<span>{% trans "Global" %}</span>{% endif %}
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{# Omit assigned IP addresses from object representation #}
|
||||
@@ -11,75 +8,3 @@
|
||||
{{ block.super }}
|
||||
<li class="breadcrumb-item"><a href="{% url 'ipam:fhrpgroup_list' %}?protocol={{ object.protocol }}">{{ object.get_protocol_display }}</a></li>
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "FHRP Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Protocol" %}</th>
|
||||
<td>{{ object.get_protocol_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group ID" %}</th>
|
||||
<td>{{ object.group_id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Members" %}</th>
|
||||
<td>{{ member_count }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Authentication" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Authentication Type" %}</th>
|
||||
<td>{{ object.get_auth_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Authentication Key" %}</th>
|
||||
<td>{{ object.auth_key|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Virtual IP Addresses" %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?fhrpgroup={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add IP Address" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:ipaddress_list' fhrpgroup_id=object.pk %}
|
||||
</div>
|
||||
{% include 'inc/panel_table.html' with table=members_table heading='Members' %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,129 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-4">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IP Address" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>
|
||||
{% if object.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
|
||||
{% else %}
|
||||
<span>{% trans "Global" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>
|
||||
{% if object.role %}
|
||||
<a href="{% url 'ipam:ipaddress_list' %}?role={{ object.role }}">{{ object.get_role_display }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "DNS Name" %}</th>
|
||||
<td>{{ object.dns_name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Assignment" %}</th>
|
||||
<td>
|
||||
{% if object.assigned_object %}
|
||||
{% if object.assigned_object.parent_object %}
|
||||
{{ object.assigned_object.parent_object|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.assigned_object|linkify }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "NAT (inside)" %}</th>
|
||||
<td>
|
||||
{% if object.nat_inside %}
|
||||
{{ object.nat_inside|linkify }}
|
||||
{% if object.nat_inside.assigned_object %}
|
||||
({{ object.nat_inside.assigned_object.parent_object|linkify }})
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "NAT (outside)" %}</th>
|
||||
<td>
|
||||
{% for ip in object.nat_outside.all %}
|
||||
{{ ip|linkify }}
|
||||
{% if ip.assigned_object %}
|
||||
({{ ip.assigned_object.parent_object|linkify }})
|
||||
{% endif %}<br/>
|
||||
{% empty %}
|
||||
{{ ''|placeholder }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Primary IP</th>
|
||||
<td>{% checkmark object.is_primary_ip %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">OOB IP</th>
|
||||
<td>{% checkmark object.is_oob_ip %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-8">
|
||||
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
||||
{% if duplicate_ips_table.rows %}
|
||||
{% include 'inc/panel_table.html' with table=duplicate_ips_table heading='Duplicate IPs' panel_class='danger' %}
|
||||
{% endif %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Application Services" %}</h2>
|
||||
{% htmx_table 'ipam:service_list' ip_address_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% extends 'ipam/ipaddress/base.html' %}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{% if value.parent_object %}{{ value.parent_object|linkify }} / {% endif %}{{ value|linkify }}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{{ value|linkify }}{% if value.assigned_object %} ({{ value.assigned_object.parent_object|linkify }}){% endif %}
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{% for ip in value.all %}{{ ip|linkify }}{% if ip.assigned_object %} ({{ ip.assigned_object.parent_object|linkify }}){% endif %}<br/>{% empty %}<span class="text-muted">—</span>{% endfor %}
|
||||
@@ -1,98 +1 @@
|
||||
{% extends 'ipam/iprange/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IP Range" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Starting Address" %}</th>
|
||||
<td>{{ object.start_address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Ending Address" %}</th>
|
||||
<td>{{ object.end_address }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Size" %}</th>
|
||||
<td>{{ object.size }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Marked Populated" %}</th>
|
||||
<td>{% checkmark object.mark_populated %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Marked Utilized" %}</th>
|
||||
<td>{% checkmark object.mark_utilized %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Utilization" %}</th>
|
||||
<td>
|
||||
{% if object.mark_utilized %}
|
||||
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
|
||||
{% else %}
|
||||
{% utilization_graph object.utilization %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>
|
||||
{% if object.vrf %}
|
||||
{{ object.vrf|linkify }} ({{ object.vrf.rd }})
|
||||
{% else %}
|
||||
<span>{% trans "Global" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% include 'inc/panel_table.html' with table=parent_prefixes_table heading='Parent Prefixes' %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{% load helpers %}
|
||||
{% if object.mark_utilized %}
|
||||
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
|
||||
{% else %}
|
||||
{% utilization_graph value %}
|
||||
{% endif %}
|
||||
@@ -0,0 +1,76 @@
|
||||
{% load helpers i18n %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Prefix" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>
|
||||
{% if object.vrf %}
|
||||
{{ object.vrf|linkify }}
|
||||
{% else %}
|
||||
<span>{% trans "Global" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Aggregate" %}</th>
|
||||
<td>
|
||||
{% if aggregate %}
|
||||
{{ aggregate|linkify }} ({{ aggregate.rir }})
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Scope" %}</th>
|
||||
{% if object.scope %}
|
||||
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN" %}</th>
|
||||
<td>
|
||||
{% if object.vlan %}
|
||||
{% if object.vlan.group %}
|
||||
{{ object.vlan.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.vlan|linkify }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Is a pool" %}</th>
|
||||
<td>{% checkmark object.is_pool %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,62 @@
|
||||
{% load humanize helpers i18n %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Addressing" %}
|
||||
{% if object.prefix.version == 4 %}
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-ghost-primary btn-sm" data-bs-toggle="modal" data-bs-target="#prefix-modal">
|
||||
<i class="mdi mdi-information-outline" aria-hidden="true"></i> {% trans "Addressing Details" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Utilization" %}</th>
|
||||
<td>
|
||||
{% if object.mark_utilized %}
|
||||
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
|
||||
<small>({% trans "Marked fully utilized" %})</small>
|
||||
{% else %}
|
||||
{% utilization_graph object.get_utilization %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% with child_ip_count=object.get_child_ips.count %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Child IPs" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">{{ child_ip_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% with available_count=object.get_available_ips.size %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Available IPs" %}</th>
|
||||
<td>
|
||||
{% if available_count > 1000000 %}
|
||||
{{ available_count|intword }}
|
||||
{% else %}
|
||||
{{ available_count|intcomma }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "First available IP" %}</th>
|
||||
<td>
|
||||
{% with first_available_ip=object.get_first_available_ip %}
|
||||
{% if first_available_ip %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}{% if object.vrf %}&vrf={{ object.vrf_id }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}">{{ first_available_ip }}</a>
|
||||
{% else %}
|
||||
{{ first_available_ip }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,202 +1 @@
|
||||
{% extends 'ipam/prefix/base.html' %}
|
||||
{% load humanize %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Prefix" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Family" %}</th>
|
||||
<td>IPv{{ object.family }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VRF" %}</th>
|
||||
<td>
|
||||
{% if object.vrf %}
|
||||
<a href="{% url 'ipam:vrf' pk=object.vrf.pk %}">{{ object.vrf }}</a>
|
||||
{% else %}
|
||||
<span>{% trans "Global" %}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Aggregate" %}</th>
|
||||
<td>
|
||||
{% if aggregate %}
|
||||
<a href="{% url 'ipam:aggregate' pk=aggregate.pk %}">{{ aggregate.prefix }}</a> ({{ aggregate.rir }})
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Scope" %}</th>
|
||||
{% if object.scope %}
|
||||
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN" %}</th>
|
||||
<td>
|
||||
{% if object.vlan %}
|
||||
{% if object.vlan.group %}
|
||||
{{ object.vlan.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.vlan|linkify }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ object.role|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Is a pool" %}</th>
|
||||
<td>{% checkmark object.is_pool %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Addressing" %}
|
||||
{% if object.prefix.version == 4 %}
|
||||
<div class="card-actions">
|
||||
<a class="btn btn-ghost-primary btn-sm" data-bs-toggle="modal" data-bs-target="#prefix-modal">
|
||||
<i class="mdi mdi-information-outline" aria-hidden="true"></i> {% trans "Addressing Details" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Utilization" %}</th>
|
||||
<td>
|
||||
{% if object.mark_utilized %}
|
||||
{% utilization_graph 100 warning_threshold=0 danger_threshold=0 %}
|
||||
<small>({% trans "Marked fully utilized" %})</small>
|
||||
{% else %}
|
||||
{% utilization_graph object.get_utilization %}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% with child_ip_count=object.get_child_ips.count %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Child IPs" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'ipam:prefix_ipaddresses' pk=object.pk %}">{{ child_ip_count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
{% with available_count=object.get_available_ips.size %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Available IPs" %}</th>
|
||||
<td>
|
||||
{# Use human-friendly words for counts greater than one million #}
|
||||
{% if available_count > 1000000 %}
|
||||
{{ available_count|intword }}
|
||||
{% else %}
|
||||
{{ available_count|intcomma }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endwith %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "First available IP" %}</th>
|
||||
<td>
|
||||
{% with first_available_ip=object.get_first_available_ip %}
|
||||
{% if first_available_ip %}
|
||||
{% if perms.ipam.add_ipaddress %}
|
||||
<a href="{% url 'ipam:ipaddress_add' %}?address={{ first_available_ip }}{% if object.vrf %}&vrf={{ object.vrf_id }}{% endif %}{% if object.tenant %}&tenant={{ object.tenant.pk }}{% endif %}">{{ first_available_ip }}</a>
|
||||
{% else %}
|
||||
{{ first_available_ip }}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% if duplicate_prefix_table.rows %}
|
||||
{% include 'inc/panel_table.html' with table=duplicate_prefix_table heading='Duplicate Prefixes' %}
|
||||
{% endif %}
|
||||
{% include 'inc/panel_table.html' with table=parent_prefix_table heading='Parent Prefixes' %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% if object.prefix.version == 4 %}
|
||||
<div class="modal fade" id="prefix-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Prefix Details" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<table class="table table-hover attr-table m-0">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Network Address" %}</th>
|
||||
<td>{{ object.prefix.network }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Network Mask" %}</th>
|
||||
<td>{{ object.prefix.netmask }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Wildcard Mask" %}</th>
|
||||
<td>{{ object.prefix.hostmask }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Broadcast Address" %}</th>
|
||||
<td>{{ object.prefix.broadcast }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
@@ -8,3 +9,39 @@
|
||||
<li class="breadcrumb-item"><a href="{% url 'ipam:prefix_list' %}?vrf_id={{ object.vrf.pk }}">{{ object.vrf }}</a></li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block modals %}
|
||||
{{ block.super }}
|
||||
{% if object.prefix.version == 4 %}
|
||||
<div class="modal fade" id="prefix-modal" tabindex="-1" aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{% trans "Prefix Details" %}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body p-0">
|
||||
<table class="table table-hover attr-table m-0">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Network Address" %}</th>
|
||||
<td>{{ object.prefix.network }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Network Mask" %}</th>
|
||||
<td>{{ object.prefix.netmask }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Wildcard Mask" %}</th>
|
||||
<td>{{ object.prefix.hostmask }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Broadcast Address" %}</th>
|
||||
<td>{{ object.prefix.broadcast }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock modals %}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@@ -11,40 +8,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "RIR" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Private" %}</th>
|
||||
<td>{% checkmark object.is_private %}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@@ -11,40 +8,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Role" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Weight" %}</th>
|
||||
<td>{{ object.weight }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,68 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Route Target" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td class="font-monospace">{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>{{ object.tenant|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Importing VRFs" %}</h2>
|
||||
{% htmx_table 'ipam:vrf_list' import_target_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Exporting VRFs" %}</h2>
|
||||
{% htmx_table 'ipam:vrf_list' export_target_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Importing L2VPNs" %}</h2>
|
||||
{% htmx_table 'vpn:l2vpn_list' import_target_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Exporting L2VPNs" %}</h2>
|
||||
{% htmx_table 'vpn:l2vpn_list' export_target_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
@@ -14,58 +10,4 @@
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Service" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Protocol" %}</th>
|
||||
<td>{{ object.get_protocol_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Ports" %}</th>
|
||||
<td>{{ object.port_list }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "IP Addresses" %}</th>
|
||||
<td>
|
||||
{% for ipaddress in object.ipaddresses.all %}
|
||||
{{ ipaddress|linkify }}<br />
|
||||
{% empty %}
|
||||
{{ ''|placeholder }}
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock breadcrumbs %}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
{% load helpers %}
|
||||
{% for ipaddress in value.all %}{{ ipaddress|linkify }}<br />{% empty %}<span class="text-muted">—</span>{% endfor %}
|
||||
@@ -1,46 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load perms %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Application Service Template" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Protocol" %}</th>
|
||||
<td>{{ object.get_protocol_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Ports" %}</th>
|
||||
<td>{{ object.port_list }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,129 +1 @@
|
||||
{% extends 'ipam/vlan/base.html' %}
|
||||
{% load helpers %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
{% load mptt %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VLAN" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
{% if object.site.region %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Region" %}</th>
|
||||
<td>
|
||||
{% nested_tree object.site.region %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Site" %}</th>
|
||||
<td>{{ object.site|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group" %}</th>
|
||||
<td>{{ object.group|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN ID" %}</th>
|
||||
<td>{{ object.vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>
|
||||
{% if object.role %}
|
||||
<a href="{% url 'ipam:vlan_list' %}?role={{ object.role.slug }}">{{ object.role }}</a>
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Q-in-Q Role" %}</th>
|
||||
<td>
|
||||
{% if object.qinq_role %}
|
||||
{% badge object.get_qinq_role_display bg_color=object.get_qinq_role_color %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% if object.qinq_role == 'cvlan' %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "Q-in-Q SVLAN" %}</th>
|
||||
<td>{{ object.qinq_svlan|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th scope="row">{% trans "L2VPN" %}</th>
|
||||
<td>{{ object.l2vpn_termination.l2vpn|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Prefixes" %}
|
||||
{% if perms.ipam.add_prefix %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'ipam:prefix_add' %}?{% if object.tenant %}tenant={{ object.tenant.pk }}&{% endif %}site={{ object.site.pk }}&vlan={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Prefix" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:prefix_list' vlan_id=object.pk %}
|
||||
</div>
|
||||
{% if object.qinq_role == 'svlan' %}
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Customer VLANs" %}
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'ipam:vlan_add' %}?qinq_role=cvlan&qinq_svlan={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a VLAN" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:vlan_list' qinq_svlan_id=object.pk %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,70 +1,10 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
{{ block.super }}
|
||||
{% if object.scope %}
|
||||
{# TODO: This should link to a filtered list of VLANGroups #}
|
||||
<li class="breadcrumb-item">{{ object.scope|linkify }}</li>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.ipam.add_vlan %}
|
||||
<a href="{% url 'ipam:vlan_add' %}?group={{ object.pk }}" class="btn btn-primary">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add VLAN" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VLAN Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Scope" %}</th>
|
||||
<td>{{ object.scope|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN IDs" %}</th>
|
||||
<td>{{ object.vid_ranges_items|join:", " }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">Utilization</th>
|
||||
<td>{% utilization_graph object.utilization %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{{ value|join:", " }}
|
||||
@@ -1,65 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-4">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VLAN Translation Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Rules" %}</th>
|
||||
<td>
|
||||
{% if object.rules.count %}
|
||||
<a href="{% url 'ipam:vlantranslationrule_list' %}?policy_id={{ object.pk }}">{{ object.rules.count }}</a>
|
||||
{% else %}
|
||||
0
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-md-8">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "VLAN Translation Rules" %}
|
||||
{% if perms.ipam.add_vlantranslationrule %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'ipam:vlantranslationrule_add' %}?device={{ object.device.pk }}&policy={{ object.pk }}&return_url={{ object.get_absolute_url }}"
|
||||
class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Rule" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'ipam:vlantranslationrule_list' policy_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,45 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-4">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VLAN Translation Rule" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Policy" %}</th>
|
||||
<td>{{ object.policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Local VID" %}</th>
|
||||
<td>{{ object.local_vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Remote VID" %}</th>
|
||||
<td>{{ object.remote_vid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-8">
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,61 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load buttons %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block title %}{% trans "VRF" %} {{ object }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "VRF" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Route Distinguisher" %}</th>
|
||||
<td>{{ object.rd|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Unique IP Space" %}</th>
|
||||
<td>{% checkmark object.enforce_unique %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panel_table.html' with table=import_targets_table heading="Import Route Targets" %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panel_table.html' with table=export_targets_table heading="Export Route Targets" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -16,23 +16,23 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
|
||||
<div class="dropdown">
|
||||
{% if perms.virtualization.change_virtualmachine %}
|
||||
<div class="dropdown">
|
||||
<button id="add-components" type="button" class="btn btn-primary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add Components" %}
|
||||
</button>
|
||||
<ul class="dropdown-menu" aria-labeled-by="add-components">
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<li><a class="dropdown-item" href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">
|
||||
{% trans "Interfaces" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_virtualdisk %}
|
||||
<li><a class="dropdown-item" href="{% url 'virtualization:virtualdisk_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_disks' pk=object.pk %}">
|
||||
{% trans "Virtual Disks" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_vminterface %}
|
||||
<li><a class="dropdown-item" href="{% url 'virtualization:vminterface_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_interfaces' pk=object.pk %}">
|
||||
{% trans "Interfaces" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
{% if perms.virtualization.add_virtualdisk %}
|
||||
<li><a class="dropdown-item" href="{% url 'virtualization:virtualdisk_add' %}?virtual_machine={{ object.pk }}&return_url={% url 'virtualization:virtualmachine_disks' pk=object.pk %}">
|
||||
{% trans "Virtual Disks" %}
|
||||
</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{% load i18n %}
|
||||
<span id="secret" class="font-monospace" data-secret="{{ value }}">{{ value }}</span>
|
||||
<button type="button" class="btn btn-primary toggle-secret float-end" data-bs-toggle="button">{% trans "Show Secret" %}</button>
|
||||
@@ -1,63 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IKE Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "IKE Version" %}</th>
|
||||
<td>{{ object.get_version_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Mode" %}</th>
|
||||
<td>{{ object.get_mode_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Pre-Shared Key" %}</th>
|
||||
<td>
|
||||
<span id="secret" class="font-monospace" data-secret="{{ object.preshared_key }}">{{ object.preshared_key|placeholder }}</span>
|
||||
{% if object.preshared_key %}
|
||||
<button type="button" class="btn btn-primary toggle-secret float-end" data-bs-toggle="button">{% trans "Show Secret" %}</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "IPSec Profiles" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'vpn:ipsecprofile_list' %}?ike_policy_id={{ object.pk }}">{{ object.ipsec_profiles.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Proposals" %}</h2>
|
||||
{% htmx_table 'vpn:ikeproposal_list' ike_policy_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,62 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IKE Proposal" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Authentication method" %}</th>
|
||||
<td>{{ object.get_authentication_method_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Encryption algorithm" %}</th>
|
||||
<td>{{ object.get_encryption_algorithm_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Authentication algorithm" %}</th>
|
||||
<td>{{ object.get_authentication_algorithm_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "DH group" %}</th>
|
||||
<td>{{ object.get_group_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "SA lifetime (seconds)" %}</th>
|
||||
<td>{{ object.sa_lifetime|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "IKE Policies" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'vpn:ikepolicy_list' %}?proposal_id={{ object.pk }}">{{ object.ike_policies.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,51 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IPSec Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "PFS group" %}</th>
|
||||
<td>{{ object.get_pfs_group_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "IPSec Profiles" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'vpn:ipsecprofile_list' %}?ipsec_policy_id={{ object.pk }}">{{ object.ipsec_profiles.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Proposals" %}</h2>
|
||||
{% htmx_table 'vpn:ipsecproposal_list' ipsec_policy_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,102 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IPSec Profile" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Mode" %}</th>
|
||||
<td>{{ object.get_mode_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IKE Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.ike_policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.ike_policy.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Version" %}</th>
|
||||
<td>{{ object.ike_policy.get_version_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Mode" %}</th>
|
||||
<td>{{ object.ike_policy.get_mode_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Proposals" %}</th>
|
||||
<td>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for proposal in object.ike_policy.proposals.all %}
|
||||
<li>
|
||||
<a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IPSec Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.ipsec_policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.ipsec_policy.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Proposals" %}</th>
|
||||
<td>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for proposal in object.ipsec_policy.proposals.all %}
|
||||
<li>
|
||||
<a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "PFS Group" %}</th>
|
||||
<td>{{ object.ipsec_policy.get_pfs_group_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,58 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IPSec Proposal" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Encryption algorithm" %}</th>
|
||||
<td>{{ object.get_encryption_algorithm_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Authentication algorithm" %}</th>
|
||||
<td>{{ object.get_authentication_algorithm_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "SA lifetime (seconds)" %}</th>
|
||||
<td>{{ object.sa_lifetime_seconds|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "SA lifetime (KB)" %}</th>
|
||||
<td>{{ object.sa_lifetime_data|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "IPSec Policies" %}</th>
|
||||
<td>
|
||||
<a href="{% url 'vpn:ipsecpolicy_list' %}?proposal_id={{ object.pk }}">{{ object.ipsec_policies.count }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,78 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "L2VPN Attributes" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Identifier" %}</th>
|
||||
<td>{{ object.identifier|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>{{ object.tenant|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpn_list' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panel_table.html' with table=import_targets_table heading="Import Route Targets" %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panel_table.html' with table=export_targets_table heading="Export Route Targets" %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Terminations" %}
|
||||
{% if perms.vpn.add_l2vpntermination %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'vpn:l2vpntermination_add' %}?l2vpn={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm{% if not object.can_add_termination %} disabled" aria-disabled="true{% endif %}">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'vpn:l2vpntermination_list' l2vpn_id=object.pk %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,28 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "L2VPN Attributes" %}</h2>
|
||||
<table class="table table-hover">
|
||||
<tr>
|
||||
<th scope="row">{% trans "L2VPN" %}</th>
|
||||
<td>{{ object.l2vpn|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Assigned Object" %}</th>
|
||||
<td>{{ object.assigned_object|linkify }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' with tags=object.tags.all url='vpn:l2vpntermination_list' %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IKE Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.ike_policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.ike_policy.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Version" %}</th>
|
||||
<td>{{ object.ike_policy.get_version_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Mode" %}</th>
|
||||
<td>{{ object.ike_policy.get_mode_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Proposals" %}</th>
|
||||
<td>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for proposal in object.ike_policy.proposals.all %}
|
||||
<li><a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -0,0 +1,30 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "IPSec Policy" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.ipsec_policy|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.ipsec_policy.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Proposals" %}</th>
|
||||
<td>
|
||||
<ul class="list-unstyled mb-0">
|
||||
{% for proposal in object.ipsec_policy.proposals.all %}
|
||||
<li><a href="{{ proposal.get_absolute_url }}">{{ proposal }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "PFS Group" %}</th>
|
||||
<td>{{ object.ipsec_policy.get_pfs_group_display }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,6 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block extra_controls %}
|
||||
@@ -10,77 +8,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Tunnel" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group" %}</th>
|
||||
<td>{{ object.group|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Encapsulation" %}</th>
|
||||
<td>{{ object.get_encapsulation_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "IPSec profile" %}</th>
|
||||
<td>{{ object.ipsec_profile|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tunnel ID" %}</th>
|
||||
<td>{{ object.tunnel_id|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Terminations" %}
|
||||
{% if perms.vpn.add_tunneltermination %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'vpn:tunneltermination_add' %}?tunnel={{ object.pk }}&return_url={{ object.get_absolute_url }}" class="btn btn-ghost-primary btn-sm">
|
||||
<i class="mdi mdi-plus-thick" aria-hidden="true"></i> {% trans "Add a Termination" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'vpn:tunneltermination_list' tunnel_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,13 +1,6 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
<li class="breadcrumb-item"><a href="{% url 'vpn:tunnelgroup_list' %}">{% trans "Tunnel Groups" %}</a></li>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_controls %}
|
||||
{% if perms.vpn.add_tunnel %}
|
||||
<a href="{% url 'vpn:tunnel_add' %}?group={{ object.pk }}" class="btn btn-primary">
|
||||
@@ -15,36 +8,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Tunnel Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,57 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Tunnel Termination" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tunnel" %}</th>
|
||||
<td>{{ object.tunnel|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{% badge object.get_role_display bg_color=object.get_role_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
{% if object.termination.device %}
|
||||
{% trans "Device" %}
|
||||
{% elif object.termination.virtual_machine %}
|
||||
{% trans "Virtual Machine" %}
|
||||
{% endif %}
|
||||
</th>
|
||||
<td>{{ object.termination.parent_object|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Interface" %}</th>
|
||||
<td>{{ object.termination|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Outside IP" %}</th>
|
||||
<td>{{ object.outside_ip|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Peer Terminations" %}</h2>
|
||||
{% htmx_table 'vpn:tunneltermination_list' tunnel_id=object.tunnel.pk id__n=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
{% load i18n %}
|
||||
<span id="secret" class="font-monospace" data-secret="{{ value }}">{{ value }}</span>
|
||||
<button type="button" class="btn btn-primary toggle-secret float-end" data-bs-toggle="button">{% trans "Show Secret" %}</button>
|
||||
@@ -1,25 +0,0 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Authentication" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ object.get_auth_type_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Cipher" %}</th>
|
||||
<td>{{ object.get_auth_cipher_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "PSK" %}</th>
|
||||
<td>
|
||||
<span id="secret" class="font-monospace" data-secret="{{ object.auth_psk }}">{{ object.auth_psk|placeholder }}</span>
|
||||
{% if object.auth_psk %}
|
||||
<button type="button" class="btn btn-primary toggle-secret float-end" data-bs-toggle="button">{% trans "Show Secret" %}</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -1,51 +0,0 @@
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ interface.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Interface" %}</th>
|
||||
<td>{{ interface|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>
|
||||
{{ interface.get_type_display }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>
|
||||
{{ interface.get_rf_role_display|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Channel" %}</th>
|
||||
<td>
|
||||
{{ interface.get_rf_channel_display|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Channel Frequency" %}</th>
|
||||
<td>
|
||||
{% if interface.rf_channel_frequency %}
|
||||
{{ interface.rf_channel_frequency|floatformat:"-2" }} {% trans "MHz" context "Abbreviation for megahertz" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Channel Width" %}</th>
|
||||
<td>
|
||||
{% if interface.rf_channel_width %}
|
||||
{{ interface.rf_channel_width|floatformat:"-3" }} {% trans "MHz" context "Abbreviation for megahertz" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "ui/panels/_base.html" %}
|
||||
{% load helpers %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block panel_content %}
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Device" %}</th>
|
||||
<td>{{ interface.device|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Interface" %}</th>
|
||||
<td>{{ interface|linkify }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Type" %}</th>
|
||||
<td>{{ interface.get_type_display }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Role" %}</th>
|
||||
<td>{{ interface.get_rf_role_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Channel" %}</th>
|
||||
<td>{{ interface.get_rf_channel_display|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Channel Frequency" %}</th>
|
||||
<td>
|
||||
{% if interface.rf_channel_frequency %}
|
||||
{{ interface.rf_channel_frequency|floatformat:"-2" }} {% trans "MHz" context "Abbreviation for megahertz" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Channel Width" %}</th>
|
||||
<td>
|
||||
{% if interface.rf_channel_width %}
|
||||
{{ interface.rf_channel_width|floatformat:"-3" }} {% trans "MHz" context "Abbreviation for megahertz" %}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{% endblock panel_content %}
|
||||
@@ -1,74 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Wireless LAN" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "SSID" %}</th>
|
||||
<td>{{ object.ssid }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Group" %}</th>
|
||||
<td>{{ object.group|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Scope" %}</th>
|
||||
{% if object.scope %}
|
||||
<td>{{ object.scope|linkify }} ({% trans object.scope_type.name %})</td>
|
||||
{% else %}
|
||||
<td>{{ ''|placeholder }}</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "VLAN" %}</th>
|
||||
<td>{{ object.vlan|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'wireless/inc/authentication_attrs.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Attached Interfaces" %}</h2>
|
||||
<div class="card-body table-responsive">
|
||||
{% render_table interfaces_table 'inc/table.html' %}
|
||||
{% include 'inc/paginator.html' with paginator=interfaces_table.paginator page=interfaces_table.page %}
|
||||
</div>
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load render_table from django_tables2 %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block breadcrumbs %}
|
||||
@@ -18,53 +15,3 @@
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endblock extra_controls %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row mb-3">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Wireless LAN Group" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Name" %}</th>
|
||||
<td>{{ object.name }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Parent" %}</th>
|
||||
<td>{{ object.parent|linkify|placeholder }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
{% include 'inc/panels/related_objects.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-3">
|
||||
<div class="col col-md-12">
|
||||
<div class="card">
|
||||
<h2 class="card-header">
|
||||
{% trans "Child Groups" %}
|
||||
{% if perms.wireless.add_wirelesslangroup %}
|
||||
<div class="card-actions">
|
||||
<a href="{% url 'wireless:wirelesslangroup_add' %}?parent={{ object.pk }}" class="btn btn-ghost-primary btn-sm">
|
||||
<span class="mdi mdi-plus-thick" aria-hidden="true"></span> {% trans "Add Wireless LAN Group" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% htmx_table 'wireless:wirelesslangroup_list' parent_id=object.pk %}
|
||||
</div>
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,68 +1 @@
|
||||
{% extends 'generic/object.html' %}
|
||||
{% load helpers %}
|
||||
{% load plugins %}
|
||||
{% load i18n %}
|
||||
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Interface" %} A</h2>
|
||||
{% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_a %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Link Properties" %}</h2>
|
||||
<table class="table table-hover attr-table">
|
||||
<tr>
|
||||
<th scope="row">{% trans "Status" %}</th>
|
||||
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "SSID" %}</th>
|
||||
<td>{{ object.ssid|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Tenant" %}</th>
|
||||
<td>
|
||||
{% if object.tenant.group %}
|
||||
{{ object.tenant.group|linkify }} /
|
||||
{% endif %}
|
||||
{{ object.tenant|linkify|placeholder }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Description" %}</th>
|
||||
<td>{{ object.description|placeholder }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">{% trans "Distance" %}</th>
|
||||
<td>
|
||||
{% if object.distance is not None %}
|
||||
{{ object.distance|floatformat }} {{ object.get_distance_unit_display }}
|
||||
{% else %}
|
||||
{{ ''|placeholder }}
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{% include 'inc/panels/tags.html' %}
|
||||
{% include 'inc/panels/comments.html' %}
|
||||
{% plugin_left_page object %}
|
||||
</div>
|
||||
<div class="col col-12 col-md-6">
|
||||
<div class="card">
|
||||
<h2 class="card-header">{% trans "Interface" %} B</h2>
|
||||
{% include 'wireless/inc/wirelesslink_interface.html' with interface=object.interface_b %}
|
||||
</div>
|
||||
{% include 'wireless/inc/authentication_attrs.html' %}
|
||||
{% include 'inc/panels/custom_fields.html' %}
|
||||
{% plugin_right_page object %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col col-md-12">
|
||||
{% plugin_full_width_page object %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -74,8 +74,8 @@ class ClusterImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
class Meta:
|
||||
model = Cluster
|
||||
fields = (
|
||||
'name', 'type', 'group', 'status', 'scope_type', 'scope_id', 'tenant', 'description', 'owner', 'comments',
|
||||
'tags',
|
||||
'name', 'type', 'group', 'status', 'scope_type', 'scope_name', 'scope_id', 'tenant', 'description', 'owner',
|
||||
'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': _('Scope ID'),
|
||||
|
||||
@@ -157,12 +157,20 @@ class ClusterTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"name,type,status",
|
||||
"Cluster 4,Cluster Type 1,active",
|
||||
"Cluster 5,Cluster Type 1,active",
|
||||
"Cluster 6,Cluster Type 1,active",
|
||||
)
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"name,type,status,scope_type,scope_id",
|
||||
f"Cluster 4,Cluster Type 1,active,dcim.site,{sites[0].pk}",
|
||||
f"Cluster 5,Cluster Type 1,active,dcim.site,{sites[0].pk}",
|
||||
f"Cluster 6,Cluster Type 1,active,dcim.site,{sites[0].pk}",
|
||||
),
|
||||
'scope_name': (
|
||||
"name,type,status,scope_type,scope_name",
|
||||
f"Cluster 4,Cluster Type 1,active,dcim.site,{sites[0].name}",
|
||||
f"Cluster 5,Cluster Type 1,active,dcim.site,{sites[0].name}",
|
||||
f"Cluster 6,Cluster Type 1,active,dcim.site,{sites[0].name}",
|
||||
),
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,name,comments",
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import attrs, panels
|
||||
|
||||
|
||||
class TunnelGroupPanel(panels.OrganizationalObjectPanel):
|
||||
pass
|
||||
|
||||
|
||||
class TunnelPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
group = attrs.RelatedObjectAttr('group', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
encapsulation = attrs.ChoiceAttr('encapsulation')
|
||||
ipsec_profile = attrs.RelatedObjectAttr('ipsec_profile', linkify=True, label=_('IPSec profile'))
|
||||
tunnel_id = attrs.TextAttr('tunnel_id', label=_('Tunnel ID'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
|
||||
|
||||
class TunnelTerminationPanel(panels.ObjectAttributesPanel):
|
||||
tunnel = attrs.RelatedObjectAttr('tunnel', linkify=True)
|
||||
role = attrs.ChoiceAttr('role')
|
||||
parent_object = attrs.RelatedObjectAttr(
|
||||
'termination.parent_object', linkify=True, label=_('Parent')
|
||||
)
|
||||
termination = attrs.RelatedObjectAttr('termination', linkify=True, label=_('Interface'))
|
||||
outside_ip = attrs.RelatedObjectAttr('outside_ip', linkify=True, label=_('Outside IP'))
|
||||
|
||||
|
||||
class IKEProposalPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
authentication_method = attrs.ChoiceAttr('authentication_method', label=_('Authentication method'))
|
||||
encryption_algorithm = attrs.ChoiceAttr('encryption_algorithm', label=_('Encryption algorithm'))
|
||||
authentication_algorithm = attrs.ChoiceAttr('authentication_algorithm', label=_('Authentication algorithm'))
|
||||
group = attrs.ChoiceAttr('group', label=_('DH group'))
|
||||
sa_lifetime = attrs.TextAttr('sa_lifetime', label=_('SA lifetime (seconds)'))
|
||||
|
||||
|
||||
class IKEPolicyPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
version = attrs.ChoiceAttr('version', label=_('IKE version'))
|
||||
mode = attrs.ChoiceAttr('mode')
|
||||
preshared_key = attrs.TemplatedAttr(
|
||||
'preshared_key',
|
||||
label=_('Pre-shared key'),
|
||||
template_name='vpn/attrs/preshared_key.html',
|
||||
)
|
||||
|
||||
|
||||
class IPSecProposalPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
encryption_algorithm = attrs.ChoiceAttr('encryption_algorithm', label=_('Encryption algorithm'))
|
||||
authentication_algorithm = attrs.ChoiceAttr('authentication_algorithm', label=_('Authentication algorithm'))
|
||||
sa_lifetime_seconds = attrs.TextAttr('sa_lifetime_seconds', label=_('SA lifetime (seconds)'))
|
||||
sa_lifetime_data = attrs.TextAttr('sa_lifetime_data', label=_('SA lifetime (KB)'))
|
||||
|
||||
|
||||
class IPSecPolicyPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
pfs_group = attrs.ChoiceAttr('pfs_group', label=_('PFS group'))
|
||||
|
||||
|
||||
class IPSecProfilePanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
description = attrs.TextAttr('description')
|
||||
mode = attrs.ChoiceAttr('mode')
|
||||
|
||||
|
||||
class L2VPNPanel(panels.ObjectAttributesPanel):
|
||||
name = attrs.TextAttr('name')
|
||||
identifier = attrs.TextAttr('identifier')
|
||||
type = attrs.ChoiceAttr('type')
|
||||
status = attrs.ChoiceAttr('status')
|
||||
description = attrs.TextAttr('description')
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True)
|
||||
|
||||
|
||||
class L2VPNTerminationPanel(panels.ObjectAttributesPanel):
|
||||
l2vpn = attrs.RelatedObjectAttr('l2vpn', linkify=True, label=_('L2VPN'))
|
||||
assigned_object = attrs.RelatedObjectAttr('assigned_object', linkify=True, label=_('Assigned object'))
|
||||
+209
-2
@@ -1,11 +1,24 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from extras.ui.panels import CustomFieldsPanel, TagsPanel
|
||||
from ipam.tables import RouteTargetTable
|
||||
from netbox.object_actions import AddObject, BulkDelete, BulkEdit, BulkExport, BulkImport
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
CommentsPanel,
|
||||
ContextTablePanel,
|
||||
ObjectsTablePanel,
|
||||
PluginContentPanel,
|
||||
RelatedObjectsPanel,
|
||||
TemplatePanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
from .ui import panels
|
||||
|
||||
#
|
||||
# Tunnel groups
|
||||
@@ -25,6 +38,17 @@ class TunnelGroupListView(generic.ObjectListView):
|
||||
@register_model_view(TunnelGroup)
|
||||
class TunnelGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = TunnelGroup.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.TunnelGroupPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CommentsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
@@ -92,6 +116,30 @@ class TunnelListView(generic.ObjectListView):
|
||||
@register_model_view(Tunnel)
|
||||
class TunnelView(generic.ObjectView):
|
||||
queryset = Tunnel.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.TunnelPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'vpn.tunneltermination',
|
||||
filters={'tunnel_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'vpn.tunneltermination',
|
||||
url_params={'tunnel': lambda ctx: ctx['object'].pk},
|
||||
label=_('Add a Termination'),
|
||||
),
|
||||
],
|
||||
title=_('Terminations'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(Tunnel, 'add', detail=False)
|
||||
@@ -160,6 +208,25 @@ class TunnelTerminationListView(generic.ObjectListView):
|
||||
@register_model_view(TunnelTermination)
|
||||
class TunnelTerminationView(generic.ObjectView):
|
||||
queryset = TunnelTermination.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.TunnelTerminationPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'vpn.tunneltermination',
|
||||
filters={
|
||||
'tunnel_id': lambda ctx: ctx['object'].tunnel.pk,
|
||||
'id__n': lambda ctx: ctx['object'].pk,
|
||||
},
|
||||
title=_('Peer Terminations'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(TunnelTermination, 'add', detail=False)
|
||||
@@ -210,6 +277,23 @@ class IKEProposalListView(generic.ObjectListView):
|
||||
@register_model_view(IKEProposal)
|
||||
class IKEProposalView(generic.ObjectView):
|
||||
queryset = IKEProposal.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.IKEProposalPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'vpn.ikepolicy',
|
||||
filters={'ike_proposal_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('IKE Policies'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(IKEProposal, 'add', detail=False)
|
||||
@@ -264,8 +348,31 @@ class IKEPolicyListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(IKEPolicy)
|
||||
class IKEPolicyView(generic.ObjectView):
|
||||
class IKEPolicyView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = IKEPolicy.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.IKEPolicyPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
TagsPanel(),
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'vpn.ikeproposal',
|
||||
filters={'ike_policy_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Proposals'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(IKEPolicy, 'add', detail=False)
|
||||
@@ -322,6 +429,23 @@ class IPSecProposalListView(generic.ObjectListView):
|
||||
@register_model_view(IPSecProposal)
|
||||
class IPSecProposalView(generic.ObjectView):
|
||||
queryset = IPSecProposal.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.IPSecProposalPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'vpn.ipsecpolicy',
|
||||
filters={'ipsec_proposal_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('IPSec Policies'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(IPSecProposal, 'add', detail=False)
|
||||
@@ -376,8 +500,31 @@ class IPSecPolicyListView(generic.ObjectListView):
|
||||
|
||||
|
||||
@register_model_view(IPSecPolicy)
|
||||
class IPSecPolicyView(generic.ObjectView):
|
||||
class IPSecPolicyView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = IPSecPolicy.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.IPSecPolicyPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
TagsPanel(),
|
||||
RelatedObjectsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
'vpn.ipsecproposal',
|
||||
filters={'ipsec_policy_id': lambda ctx: ctx['object'].pk},
|
||||
title=_('Proposals'),
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
return {
|
||||
'related_models': self.get_related_models(request, instance),
|
||||
}
|
||||
|
||||
|
||||
@register_model_view(IPSecPolicy, 'add', detail=False)
|
||||
@@ -434,6 +581,18 @@ class IPSecProfileListView(generic.ObjectListView):
|
||||
@register_model_view(IPSecProfile)
|
||||
class IPSecProfileView(generic.ObjectView):
|
||||
queryset = IPSecProfile.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.IPSecProfilePanel(),
|
||||
TagsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
TemplatePanel('vpn/panels/ipsecprofile_ike_policy.html'),
|
||||
TemplatePanel('vpn/panels/ipsecprofile_ipsec_policy.html'),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(IPSecProfile, 'add', detail=False)
|
||||
@@ -490,6 +649,45 @@ class L2VPNListView(generic.ObjectListView):
|
||||
@register_model_view(L2VPN)
|
||||
class L2VPNView(generic.ObjectView):
|
||||
queryset = L2VPN.objects.all()
|
||||
layout = layout.Layout(
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
panels.L2VPNPanel(),
|
||||
TagsPanel(),
|
||||
PluginContentPanel('left_page'),
|
||||
),
|
||||
layout.Column(
|
||||
CustomFieldsPanel(),
|
||||
CommentsPanel(),
|
||||
PluginContentPanel('right_page'),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
ContextTablePanel('import_targets_table', title=_('Import Route Targets')),
|
||||
),
|
||||
layout.Column(
|
||||
ContextTablePanel('export_targets_table', title=_('Export Route Targets')),
|
||||
),
|
||||
),
|
||||
layout.Row(
|
||||
layout.Column(
|
||||
ObjectsTablePanel(
|
||||
'vpn.l2vpntermination',
|
||||
filters={'l2vpn_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'vpn.l2vpntermination',
|
||||
url_params={'l2vpn': lambda ctx: ctx['object'].pk},
|
||||
label=_('Add a Termination'),
|
||||
),
|
||||
],
|
||||
title=_('Terminations'),
|
||||
),
|
||||
PluginContentPanel('full_width_page'),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
import_targets_table = RouteTargetTable(
|
||||
@@ -564,6 +762,15 @@ class L2VPNTerminationListView(generic.ObjectListView):
|
||||
@register_model_view(L2VPNTermination)
|
||||
class L2VPNTerminationView(generic.ObjectView):
|
||||
queryset = L2VPNTermination.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.L2VPNTerminationPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
CustomFieldsPanel(),
|
||||
TagsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(L2VPNTermination, 'add', detail=False)
|
||||
|
||||
@@ -76,7 +76,7 @@ class WirelessLANImportForm(ScopedImportForm, PrimaryModelImportForm):
|
||||
model = WirelessLAN
|
||||
fields = (
|
||||
'ssid', 'group', 'status', 'vlan', 'tenant', 'auth_type', 'auth_cipher', 'auth_psk', 'scope_type',
|
||||
'scope_id', 'description', 'owner', 'comments', 'tags',
|
||||
'scope_name', 'scope_id', 'description', 'owner', 'comments', 'tags',
|
||||
)
|
||||
labels = {
|
||||
'scope_id': _('Scope ID'),
|
||||
|
||||
@@ -116,23 +116,42 @@ class WirelessLANTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||
'tags': [t.pk for t in tags],
|
||||
}
|
||||
|
||||
cls.csv_data = (
|
||||
"group,ssid,status,tenant,scope_type,scope_id",
|
||||
"Wireless LAN Group 2,WLAN4,{status},{tenant},,".format(
|
||||
status=WirelessLANStatusChoices.STATUS_ACTIVE,
|
||||
tenant=tenants[0].name
|
||||
cls.csv_data = {
|
||||
'default': (
|
||||
"group,ssid,status,tenant,scope_type,scope_id",
|
||||
"Wireless LAN Group 2,WLAN4,{status},{tenant},,".format(
|
||||
status=WirelessLANStatusChoices.STATUS_ACTIVE,
|
||||
tenant=tenants[0].name
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN5,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_DISABLED,
|
||||
tenant=tenants[1].name,
|
||||
site=sites[0].pk
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN6,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_RESERVED,
|
||||
tenant=tenants[2].name,
|
||||
site=sites[1].pk
|
||||
),
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN5,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_DISABLED,
|
||||
tenant=tenants[1].name,
|
||||
site=sites[0].pk
|
||||
'scope_name': (
|
||||
"group,ssid,status,tenant,scope_type,scope_name",
|
||||
"Wireless LAN Group 2,WLAN4,{status},{tenant},,".format(
|
||||
status=WirelessLANStatusChoices.STATUS_ACTIVE,
|
||||
tenant=tenants[0].name
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN5,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_DISABLED,
|
||||
tenant=tenants[1].name,
|
||||
site=sites[0].name
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN6,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_RESERVED,
|
||||
tenant=tenants[2].name,
|
||||
site=sites[1].name
|
||||
),
|
||||
),
|
||||
"Wireless LAN Group 2,WLAN6,{status},{tenant},dcim.site,{site}".format(
|
||||
status=WirelessLANStatusChoices.STATUS_RESERVED,
|
||||
tenant=tenants[2].name,
|
||||
site=sites[1].pk
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
cls.csv_update_data = (
|
||||
"id,ssid",
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from netbox.ui import attrs, panels
|
||||
|
||||
|
||||
class WirelessLANGroupPanel(panels.NestedGroupObjectPanel):
|
||||
pass
|
||||
|
||||
|
||||
class WirelessLANPanel(panels.ObjectAttributesPanel):
|
||||
ssid = attrs.TextAttr('ssid', label=_('SSID'))
|
||||
group = attrs.RelatedObjectAttr('group', linkify=True)
|
||||
status = attrs.ChoiceAttr('status')
|
||||
scope = attrs.GenericForeignKeyAttr('scope', linkify=True)
|
||||
description = attrs.TextAttr('description')
|
||||
vlan = attrs.RelatedObjectAttr('vlan', label=_('VLAN'), linkify=True)
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
|
||||
|
||||
class WirelessAuthenticationPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Authentication')
|
||||
|
||||
auth_type = attrs.ChoiceAttr('auth_type', label=_('Type'))
|
||||
auth_cipher = attrs.ChoiceAttr('auth_cipher', label=_('Cipher'))
|
||||
auth_psk = attrs.TemplatedAttr('auth_psk', label=_('PSK'), template_name='wireless/attrs/auth_psk.html')
|
||||
|
||||
|
||||
class WirelessLinkInterfacePanel(panels.ObjectPanel):
|
||||
template_name = 'wireless/panels/wirelesslink_interface.html'
|
||||
|
||||
def __init__(self, interface_attr, title, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.interface_attr = interface_attr
|
||||
self.title = title
|
||||
|
||||
def get_context(self, context):
|
||||
obj = context['object']
|
||||
return {
|
||||
**super().get_context(context),
|
||||
'interface': getattr(obj, self.interface_attr),
|
||||
}
|
||||
|
||||
|
||||
class WirelessLinkPropertiesPanel(panels.ObjectAttributesPanel):
|
||||
title = _('Link Properties')
|
||||
|
||||
status = attrs.ChoiceAttr('status')
|
||||
ssid = attrs.TextAttr('ssid', label=_('SSID'))
|
||||
tenant = attrs.RelatedObjectAttr('tenant', linkify=True, grouped_by='group')
|
||||
description = attrs.TextAttr('description')
|
||||
distance = attrs.NumericAttr('distance', unit_accessor='get_distance_unit_display')
|
||||
+68
-11
@@ -1,10 +1,20 @@
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from dcim.models import Interface
|
||||
from extras.ui.panels import CustomFieldsPanel, TagsPanel
|
||||
from netbox.ui import actions, layout
|
||||
from netbox.ui.panels import (
|
||||
CommentsPanel,
|
||||
ObjectsTablePanel,
|
||||
RelatedObjectsPanel,
|
||||
)
|
||||
from netbox.views import generic
|
||||
from utilities.query import count_related
|
||||
from utilities.views import GetRelatedModelsMixin, register_model_view
|
||||
|
||||
from . import filtersets, forms, tables
|
||||
from .models import *
|
||||
from .ui import panels
|
||||
|
||||
#
|
||||
# Wireless LAN groups
|
||||
@@ -28,6 +38,33 @@ class WirelessLANGroupListView(generic.ObjectListView):
|
||||
@register_model_view(WirelessLANGroup)
|
||||
class WirelessLANGroupView(GetRelatedModelsMixin, generic.ObjectView):
|
||||
queryset = WirelessLANGroup.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.WirelessLANGroupPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
RelatedObjectsPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='wireless.WirelessLANGroup',
|
||||
title=_('Child Groups'),
|
||||
filters={'parent_id': lambda ctx: ctx['object'].pk},
|
||||
actions=[
|
||||
actions.AddObject(
|
||||
'wireless.WirelessLANGroup',
|
||||
label=_('Add Wireless LAN Group'),
|
||||
url_params={
|
||||
'parent': lambda ctx: ctx['object'].pk,
|
||||
}
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
groups = instance.get_descendants(include_self=True)
|
||||
@@ -105,17 +142,24 @@ class WirelessLANListView(generic.ObjectListView):
|
||||
@register_model_view(WirelessLAN)
|
||||
class WirelessLANView(generic.ObjectView):
|
||||
queryset = WirelessLAN.objects.all()
|
||||
|
||||
def get_extra_context(self, request, instance):
|
||||
attached_interfaces = Interface.objects.restrict(request.user, 'view').filter(
|
||||
wireless_lans=instance
|
||||
)
|
||||
interfaces_table = tables.WirelessLANInterfacesTable(attached_interfaces)
|
||||
interfaces_table.configure(request)
|
||||
|
||||
return {
|
||||
'interfaces_table': interfaces_table,
|
||||
}
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.WirelessLANPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.WirelessAuthenticationPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
bottom_panels=[
|
||||
ObjectsTablePanel(
|
||||
model='dcim.Interface',
|
||||
title=_('Attached Interfaces'),
|
||||
filters={'wireless_lan_id': lambda ctx: ctx['object'].pk},
|
||||
),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(WirelessLAN, 'add', detail=False)
|
||||
@@ -173,6 +217,19 @@ class WirelessLinkListView(generic.ObjectListView):
|
||||
@register_model_view(WirelessLink)
|
||||
class WirelessLinkView(generic.ObjectView):
|
||||
queryset = WirelessLink.objects.all()
|
||||
layout = layout.SimpleLayout(
|
||||
left_panels=[
|
||||
panels.WirelessLinkInterfacePanel('interface_a', title=_('Interface A')),
|
||||
panels.WirelessLinkPropertiesPanel(),
|
||||
TagsPanel(),
|
||||
CommentsPanel(),
|
||||
],
|
||||
right_panels=[
|
||||
panels.WirelessLinkInterfacePanel('interface_b', title=_('Interface B')),
|
||||
panels.WirelessAuthenticationPanel(),
|
||||
CustomFieldsPanel(),
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@register_model_view(WirelessLink, 'add', detail=False)
|
||||
|
||||
Reference in New Issue
Block a user