mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-15 19:52:52 -06:00
Changed Secret parent from a GenericForeignKey to ForeignKey(Device)
This commit is contained in:
parent
3f279dc58b
commit
a6108f2fa8
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Secret
|
# Secret
|
||||||
|
|
||||||
A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a parent object with NetBox, such as a device. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
A secret represents a single credential or other string which must be stored securely. Each secret is assigned to a device within NetBox. The plaintext value of a secret is encrypted to a ciphertext immediately prior to storage within the database using a 256-bit AES master key. A SHA256 hash of the plaintext is also stored along with each ciphertext to validate the decrypted plaintext.
|
||||||
|
|
||||||
Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names.
|
Each secret can also store an optional name parameter, which is not encrypted. This may be useful for storing user names.
|
||||||
|
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from django.contrib.contenttypes.fields import GenericRelation
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.core.validators import MinValueValidator
|
from django.core.validators import MinValueValidator
|
||||||
@ -8,7 +7,6 @@ from django.db import models
|
|||||||
from django.db.models import Q, ObjectDoesNotExist
|
from django.db.models import Q, ObjectDoesNotExist
|
||||||
|
|
||||||
from extras.rpc import RPC_CLIENTS
|
from extras.rpc import RPC_CLIENTS
|
||||||
from secrets.models import Secret
|
|
||||||
from utilities.fields import NullableCharField
|
from utilities.fields import NullableCharField
|
||||||
|
|
||||||
|
|
||||||
@ -420,7 +418,6 @@ class Device(models.Model):
|
|||||||
primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IP')
|
primary_ip = models.OneToOneField('ipam.IPAddress', related_name='primary_for', on_delete=models.SET_NULL, blank=True, null=True, verbose_name='Primary IP')
|
||||||
ro_snmp = models.CharField(max_length=50, blank=True, verbose_name='SNMP (RO)')
|
ro_snmp = models.CharField(max_length=50, blank=True, verbose_name='SNMP (RO)')
|
||||||
comments = models.TextField(blank=True)
|
comments = models.TextField(blank=True)
|
||||||
secrets = GenericRelation(Secret)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
|
@ -70,8 +70,7 @@ urlpatterns = [
|
|||||||
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
|
url(r'^devices/(?P<pk>\d+)/inventory/$', views.device_inventory, name='device_inventory'),
|
||||||
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
|
url(r'^devices/(?P<pk>\d+)/lldp-neighbors/$', views.device_lldp_neighbors, name='device_lldp_neighbors'),
|
||||||
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
url(r'^devices/(?P<pk>\d+)/ip-addresses/assign/$', views.ipaddress_assign, name='ipaddress_assign'),
|
||||||
url(r'^devices/(?P<parent_pk>\d+)/add-secret/$', secret_add, {'parent_model': 'dcim.Device'},
|
url(r'^devices/(?P<pk>\d+)/add-secret/$', secret_add, name='device_addsecret'),
|
||||||
name='device_addsecret'),
|
|
||||||
|
|
||||||
# Console ports
|
# Console ports
|
||||||
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
url(r'^devices/(?P<pk>\d+)/console-ports/add/$', views.consoleport_add, name='consoleport_add'),
|
||||||
|
@ -66,6 +66,6 @@ class SecretRoleAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
@admin.register(Secret)
|
@admin.register(Secret)
|
||||||
class SecretAdmin(admin.ModelAdmin):
|
class SecretAdmin(admin.ModelAdmin):
|
||||||
list_display = ['parent', 'role', 'name', 'created', 'last_modified']
|
list_display = ['device', 'role', 'name', 'created', 'last_modified']
|
||||||
fields = ['parent', 'role', 'name', 'hash', 'created', 'last_modified']
|
fields = ['device', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||||
readonly_fields = ['parent', 'hash', 'created', 'last_modified']
|
readonly_fields = ['device', 'hash', 'created', 'last_modified']
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
|
||||||
|
from dcim.api.serializers import DeviceNestedSerializer
|
||||||
from secrets.models import Secret, SecretRole
|
from secrets.models import Secret, SecretRole
|
||||||
|
|
||||||
|
|
||||||
@ -24,13 +25,13 @@ class SecretRoleNestedSerializer(SecretRoleSerializer):
|
|||||||
# Secrets
|
# Secrets
|
||||||
#
|
#
|
||||||
|
|
||||||
# TODO: Serialize parent info
|
|
||||||
class SecretSerializer(serializers.ModelSerializer):
|
class SecretSerializer(serializers.ModelSerializer):
|
||||||
|
device = DeviceNestedSerializer()
|
||||||
role = SecretRoleNestedSerializer()
|
role = SecretRoleNestedSerializer()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['id', 'role', 'name', 'hash', 'created', 'last_modified']
|
fields = ['id', 'device', 'role', 'name', 'hash', 'created', 'last_modified']
|
||||||
|
|
||||||
|
|
||||||
class SecretNestedSerializer(SecretSerializer):
|
class SecretNestedSerializer(SecretSerializer):
|
||||||
|
@ -2,10 +2,10 @@ from Crypto.Cipher import PKCS1_OAEP
|
|||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.apps import apps
|
|
||||||
from django.db.models import Count
|
from django.db.models import Count
|
||||||
|
|
||||||
from utilities.forms import BootstrapMixin, ConfirmationForm, CSVDataField
|
from dcim.models import Device
|
||||||
|
from utilities.forms import BootstrapMixin, BulkImportForm, ConfirmationForm, CSVDataField
|
||||||
from .models import Secret, SecretRole, UserKey
|
from .models import Secret, SecretRole, UserKey
|
||||||
|
|
||||||
|
|
||||||
@ -53,51 +53,26 @@ class SecretForm(forms.ModelForm, BootstrapMixin):
|
|||||||
|
|
||||||
|
|
||||||
class SecretFromCSVForm(forms.ModelForm):
|
class SecretFromCSVForm(forms.ModelForm):
|
||||||
parent_name = forms.CharField()
|
device = forms.ModelChoiceField(queryset=Device.objects.all(), required=False, to_field_name='name',
|
||||||
|
error_messages={'invalid_choice': 'Device not found.'})
|
||||||
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
|
role = forms.ModelChoiceField(queryset=SecretRole.objects.all(), to_field_name='name',
|
||||||
error_messages={'invalid_choice': 'Invalid secret role.'})
|
error_messages={'invalid_choice': 'Invalid secret role.'})
|
||||||
plaintext = forms.CharField()
|
plaintext = forms.CharField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ['parent_name', 'role', 'name', 'plaintext']
|
fields = ['device', 'role', 'name', 'plaintext']
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
s = super(SecretFromCSVForm, self).save(*args, **kwargs)
|
||||||
|
s.plaintext = str(self.cleaned_data['plaintext'])
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
class SecretImportForm(forms.Form, BootstrapMixin):
|
class SecretImportForm(BulkImportForm, BootstrapMixin):
|
||||||
private_key = forms.CharField(widget=forms.HiddenInput())
|
private_key = forms.CharField(widget=forms.HiddenInput())
|
||||||
parent_type = forms.ChoiceField(label='Parent Type', choices=(
|
|
||||||
('dcim.Device', 'Device'),
|
|
||||||
))
|
|
||||||
csv = CSVDataField(csv_form=SecretFromCSVForm)
|
csv = CSVDataField(csv_form=SecretFromCSVForm)
|
||||||
|
|
||||||
def clean(self):
|
|
||||||
parent_type = self.cleaned_data.get('parent_type')
|
|
||||||
records = self.cleaned_data.get('csv')
|
|
||||||
if not records or not parent_type:
|
|
||||||
return
|
|
||||||
|
|
||||||
secrets = []
|
|
||||||
parent_cls = apps.get_model(parent_type)
|
|
||||||
|
|
||||||
for i, record in enumerate(records, start=1):
|
|
||||||
secret_form = SecretFromCSVForm(data=record)
|
|
||||||
if secret_form.is_valid():
|
|
||||||
s = secret_form.save(commit=False)
|
|
||||||
# Set parent
|
|
||||||
try:
|
|
||||||
s.parent = parent_cls.objects.get(name=secret_form.cleaned_data['parent_name'])
|
|
||||||
except parent_cls.DoesNotExist:
|
|
||||||
self.add_error('csv', "Invalid parent object ({})".format(secret_form.cleaned_data['parent_name']))
|
|
||||||
# Set plaintext
|
|
||||||
s.plaintext = str(secret_form.cleaned_data['plaintext'])
|
|
||||||
secrets.append(s)
|
|
||||||
else:
|
|
||||||
for field, errors in secret_form.errors.items():
|
|
||||||
for e in errors:
|
|
||||||
self.add_error('csv', "Record {} {}: {}".format(i, field, e))
|
|
||||||
|
|
||||||
self.cleaned_data['csv'] = secrets
|
|
||||||
|
|
||||||
|
|
||||||
class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
class SecretBulkEditForm(forms.Form, BootstrapMixin):
|
||||||
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
pk = forms.ModelMultipleChoiceField(queryset=Secret.objects.all(), widget=forms.MultipleHiddenInput)
|
||||||
|
31
netbox/secrets/migrations/0002_auto_20160321_1448.py
Normal file
31
netbox/secrets/migrations/0002_auto_20160321_1448.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.1 on 2016-03-21 14:48
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('dcim', '0003_auto_20160304_1642'),
|
||||||
|
('secrets', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='secret',
|
||||||
|
name='content_type',
|
||||||
|
),
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='secret',
|
||||||
|
name='object_id',
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='secret',
|
||||||
|
name='device',
|
||||||
|
field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='secrets', to='dcim.Device'),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
19
netbox/secrets/migrations/0003_auto_20160321_1524.py
Normal file
19
netbox/secrets/migrations/0003_auto_20160321_1524.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Generated by Django 1.9.1 on 2016-03-21 15:24
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('secrets', '0002_auto_20160321_1448'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name='secret',
|
||||||
|
options={'ordering': ['device', 'role', 'name'], 'permissions': (('view_secret', 'Can view secrets'),)},
|
||||||
|
),
|
||||||
|
]
|
@ -5,13 +5,13 @@ from Crypto.PublicKey import RSA
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import make_password, check_password
|
from django.contrib.auth.hashers import make_password, check_password
|
||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
|
||||||
from django.contrib.contenttypes.models import ContentType
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.core.urlresolvers import reverse
|
from django.core.urlresolvers import reverse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.encoding import force_bytes
|
from django.utils.encoding import force_bytes
|
||||||
|
|
||||||
|
from dcim.models import Device
|
||||||
|
|
||||||
|
|
||||||
def generate_master_key():
|
def generate_master_key():
|
||||||
"""
|
"""
|
||||||
@ -176,9 +176,7 @@ class Secret(models.Model):
|
|||||||
A secret string of up to 255 bytes in length, stored as both an AES256-encrypted ciphertext and an irreversible
|
A secret string of up to 255 bytes in length, stored as both an AES256-encrypted ciphertext and an irreversible
|
||||||
salted SHA256 hash (for plaintext validation).
|
salted SHA256 hash (for plaintext validation).
|
||||||
"""
|
"""
|
||||||
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
device = models.ForeignKey(Device, related_name='secrets')
|
||||||
object_id = models.PositiveIntegerField()
|
|
||||||
parent = GenericForeignKey('content_type', 'object_id')
|
|
||||||
role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT)
|
role = models.ForeignKey('SecretRole', related_name='secrets', on_delete=models.PROTECT)
|
||||||
name = models.CharField(max_length=100, blank=True)
|
name = models.CharField(max_length=100, blank=True)
|
||||||
ciphertext = models.BinaryField(editable=False, max_length=65568) # 16B IV + 2B pad length + {62-65550}B padded
|
ciphertext = models.BinaryField(editable=False, max_length=65568) # 16B IV + 2B pad length + {62-65550}B padded
|
||||||
@ -189,7 +187,7 @@ class Secret(models.Model):
|
|||||||
plaintext = None
|
plaintext = None
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
ordering = ['role', 'name']
|
ordering = ['device', 'role', 'name']
|
||||||
permissions = (
|
permissions = (
|
||||||
('view_secret', "Can view secrets"),
|
('view_secret', "Can view secrets"),
|
||||||
)
|
)
|
||||||
@ -199,8 +197,8 @@ class Secret(models.Model):
|
|||||||
super(Secret, self).__init__(*args, **kwargs)
|
super(Secret, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
if self.role and self.parent:
|
if self.role and self.device:
|
||||||
return "{} for {}".format(self.role, self.parent)
|
return "{} for {}".format(self.role, self.device)
|
||||||
return "Secret"
|
return "Secret"
|
||||||
|
|
||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
|
@ -9,14 +9,14 @@ from .models import Secret
|
|||||||
#
|
#
|
||||||
|
|
||||||
class SecretTable(tables.Table):
|
class SecretTable(tables.Table):
|
||||||
parent = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Parent')
|
device = tables.LinkColumn('secrets:secret', args=[Accessor('pk')], verbose_name='Device')
|
||||||
role = tables.Column(verbose_name='Role')
|
role = tables.Column(verbose_name='Role')
|
||||||
name = tables.Column(verbose_name='Name')
|
name = tables.Column(verbose_name='Name')
|
||||||
last_modified = tables.DateTimeColumn(verbose_name='Last modified')
|
last_modified = tables.DateTimeColumn(verbose_name='Last modified')
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Secret
|
model = Secret
|
||||||
fields = ('parent', 'role', 'name', 'last_modified')
|
fields = ('device', 'role', 'name', 'last_modified')
|
||||||
empty_text = "No secrets found."
|
empty_text = "No secrets found."
|
||||||
attrs = {
|
attrs = {
|
||||||
'class': 'table table-hover',
|
'class': 'table table-hover',
|
||||||
@ -28,4 +28,4 @@ class SecretBulkEditTable(SecretTable):
|
|||||||
|
|
||||||
class Meta(SecretTable.Meta):
|
class Meta(SecretTable.Meta):
|
||||||
model = None # django_tables2 bugfix
|
model = None # django_tables2 bugfix
|
||||||
fields = ('pk', 'parent', 'role', 'name')
|
fields = ('pk', 'device', 'role', 'name')
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from django.apps import apps
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import permission_required, login_required
|
from django.contrib.auth.decorators import permission_required, login_required
|
||||||
from django.contrib.auth.mixins import PermissionRequiredMixin
|
from django.contrib.auth.mixins import PermissionRequiredMixin
|
||||||
@ -8,6 +7,7 @@ from django.db.models import ProtectedError
|
|||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.utils.decorators import method_decorator
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from dcim.models import Device
|
||||||
from utilities.error_handlers import handle_protectederror
|
from utilities.error_handlers import handle_protectederror
|
||||||
from utilities.forms import ConfirmationForm
|
from utilities.forms import ConfirmationForm
|
||||||
from utilities.views import BulkEditView, BulkDeleteView, ObjectListView
|
from utilities.views import BulkEditView, BulkDeleteView, ObjectListView
|
||||||
@ -25,7 +25,7 @@ from .tables import SecretTable, SecretBulkEditTable
|
|||||||
|
|
||||||
@method_decorator(login_required, name='dispatch')
|
@method_decorator(login_required, name='dispatch')
|
||||||
class SecretListView(ObjectListView):
|
class SecretListView(ObjectListView):
|
||||||
queryset = Secret.objects.select_related('role').prefetch_related('parent')
|
queryset = Secret.objects.select_related('role').prefetch_related('device')
|
||||||
filter = SecretFilter
|
filter = SecretFilter
|
||||||
filter_form = SecretFilterForm
|
filter_form = SecretFilterForm
|
||||||
table = SecretTable
|
table = SecretTable
|
||||||
@ -46,13 +46,12 @@ def secret(request, pk):
|
|||||||
|
|
||||||
@permission_required('secrets.add_secret')
|
@permission_required('secrets.add_secret')
|
||||||
@userkey_required()
|
@userkey_required()
|
||||||
def secret_add(request, parent_model, parent_pk):
|
def secret_add(request, pk):
|
||||||
|
|
||||||
# Retrieve parent object
|
# Retrieve device
|
||||||
parent_cls = apps.get_model(parent_model)
|
device = get_object_or_404(Device, pk=pk)
|
||||||
parent = get_object_or_404(parent_cls, pk=parent_pk)
|
|
||||||
|
|
||||||
secret = Secret(parent=parent)
|
secret = Secret(device=device)
|
||||||
uk = UserKey.objects.get(user=request.user)
|
uk = UserKey.objects.get(user=request.user)
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
@ -83,7 +82,7 @@ def secret_add(request, parent_model, parent_pk):
|
|||||||
return render(request, 'secrets/secret_edit.html', {
|
return render(request, 'secrets/secret_edit.html', {
|
||||||
'secret': secret,
|
'secret': secret,
|
||||||
'form': form,
|
'form': form,
|
||||||
'cancel_url': parent.get_absolute_url(),
|
'cancel_url': device.get_absolute_url(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@ -132,7 +132,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
</form>
|
</form>
|
||||||
<div class="panel-footer text-right">
|
<div class="panel-footer text-right">
|
||||||
<a href="{% url 'dcim:device_addsecret' parent_pk=device.pk %}" class="btn btn-xs btn-primary">
|
<a href="{% url 'dcim:device_addsecret' pk=device.pk %}" class="btn btn-xs btn-primary">
|
||||||
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
<span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
|
||||||
Add secret
|
Add secret
|
||||||
</a>
|
</a>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
|
<li><a href="{% url 'secrets:secret_list' %}">Secrets</a></li>
|
||||||
<li>{{ secret.parent }}</li>
|
<li><a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a></li>
|
||||||
<li>{{ secret.role }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
|
<li>{{ secret.role }}{% if secret.name %} ({{ secret.name }}){% endif %}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
@ -35,9 +35,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<table class="table table-hover panel-body">
|
<table class="table table-hover panel-body">
|
||||||
<tr>
|
<tr>
|
||||||
<td>Parent</td>
|
<td>Device</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{{ secret.parent.get_absolute_url }}">{{ secret.parent }}</a>
|
<a href="{% url 'dcim:device' pk=secret.device.pk %}">{{ secret.device }}</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
{% for secret in selected_objects %}
|
{% for secret in selected_objects %}
|
||||||
<tr>
|
<tr>
|
||||||
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret }}</a></td>
|
<td><a href="{% url 'secrets:secret' pk=secret.pk %}">{{ secret }}</a></td>
|
||||||
<td>{{ secret.parent }}</td>
|
<td>{{ secret.device }}</td>
|
||||||
<td>{{ secret.role }}</td>
|
<td>{{ secret.role }}</td>
|
||||||
<td>{{ secret.name }}</td>
|
<td>{{ secret.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -30,9 +30,9 @@
|
|||||||
<div class="panel-heading"><strong>Secret Attributes</strong></div>
|
<div class="panel-heading"><strong>Secret Attributes</strong></div>
|
||||||
<div class="panel-body">
|
<div class="panel-body">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-md-3 control-label required">Parent</label>
|
<label class="col-md-3 control-label required">Device</label>
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<p class="form-control-static">{{ secret.parent }}</p>
|
<p class="form-control-static">{{ secret.device }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% render_field form.role %}
|
{% render_field form.role %}
|
||||||
|
@ -37,8 +37,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Parent</td>
|
<td>Device</td>
|
||||||
<td>Name of the parent object</td>
|
<td>Name of the parent device</td>
|
||||||
<td>edge-router1</td>
|
<td>edge-router1</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
Loading…
Reference in New Issue
Block a user