diff --git a/netbox/secrets/forms.py b/netbox/secrets/forms.py
index 8f04edc5b..b391b59e6 100644
--- a/netbox/secrets/forms.py
+++ b/netbox/secrets/forms.py
@@ -11,6 +11,7 @@ from utilities.forms import (
BootstrapMixin, CSVModelChoiceField, CSVModelForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
SlugField, TagFilterField,
)
+from virtualization.models import VirtualMachine
from .constants import *
from .models import Secret, SecretRole, UserKey
@@ -64,8 +65,13 @@ class SecretRoleCSVForm(CSVModelForm):
class SecretForm(BootstrapMixin, CustomFieldModelForm):
device = DynamicModelChoiceField(
queryset=Device.objects.all(),
+ required=False,
display_field='display_name'
)
+ virtual_machine = DynamicModelChoiceField(
+ queryset=VirtualMachine.objects.all(),
+ required=False
+ )
plaintext = forms.CharField(
max_length=SECRET_PLAINTEXT_MAX_LENGTH,
required=False,
@@ -93,10 +99,21 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
class Meta:
model = Secret
fields = [
- 'device', 'role', 'name', 'plaintext', 'plaintext2', 'tags',
+ 'device', 'virtual_machine', 'role', 'name', 'plaintext', 'plaintext2', 'tags',
]
def __init__(self, *args, **kwargs):
+
+ # Initialize helper selectors
+ instance = kwargs.get('instance')
+ initial = kwargs.get('initial', {}).copy()
+ if instance:
+ if type(instance.assigned_object) is Device:
+ initial['device'] = instance.assigned_object
+ elif type(instance.assigned_object) is VirtualMachine:
+ initial['virtual_machine'] = instance.assigned_object
+ kwargs['initial'] = initial
+
super().__init__(*args, **kwargs)
# A plaintext value is required when creating a new Secret
@@ -105,21 +122,23 @@ class SecretForm(BootstrapMixin, CustomFieldModelForm):
def clean(self):
+ if not self.cleaned_data['device'] and not self.cleaned_data['virtual_machine']:
+ raise forms.ValidationError("Secrets must be assigned to a device or virtual machine.")
+
+ if self.cleaned_data['device'] and self.cleaned_data['virtual_machine']:
+ raise forms.ValidationError("Cannot select both a device and virtual machine for secret assignment.")
+
# Verify that the provided plaintext values match
if self.cleaned_data['plaintext'] != self.cleaned_data['plaintext2']:
raise forms.ValidationError({
'plaintext2': "The two given plaintext values do not match. Please check your input."
})
- # Validate uniqueness
- if Secret.objects.filter(
- device=self.cleaned_data['device'],
- role=self.cleaned_data['role'],
- name=self.cleaned_data['name']
- ).exclude(pk=self.instance.pk).exists():
- raise forms.ValidationError(
- "Each secret assigned to a device must have a unique combination of role and name"
- )
+ def save(self, *args, **kwargs):
+ # Set assigned object
+ self.instance.assigned_object = self.cleaned_data.get('device') or self.cleaned_data.get('virtual_machine')
+
+ return super().save(*args, **kwargs)
class SecretCSVForm(CustomFieldModelCSVForm):
diff --git a/netbox/secrets/views.py b/netbox/secrets/views.py
index e3d607755..2872616b8 100644
--- a/netbox/secrets/views.py
+++ b/netbox/secrets/views.py
@@ -82,13 +82,6 @@ class SecretEditView(ObjectEditView):
model_form = forms.SecretForm
template_name = 'secrets/secret_edit.html'
- def alter_obj(self, secret, request, args, kwargs):
- if not secret.pk:
- # Set assigned_object based on URL kwargs
- model = kwargs.get('model')
- secret.assigned_object = get_object_or_404(model, pk=kwargs['object_id'])
- return secret
-
def dispatch(self, request, *args, **kwargs):
# Check that the user has a valid UserKey
diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html
index ff82a49e2..6f8ae69c6 100644
--- a/netbox/templates/dcim/device.html
+++ b/netbox/templates/dcim/device.html
@@ -395,34 +395,8 @@
{% endif %}
- {% if request.user.is_authenticated %}
-
-
- Secrets
-
- {% if secrets %}
-
- {% for secret in secrets %}
- {% include 'secrets/inc/secret_tr.html' %}
- {% endfor %}
-
- {% else %}
-
- None found
-
- {% endif %}
- {% if perms.secrets.add_secret %}
-
-
- {% endif %}
-
+ {% if perms.secrets.view_secret %}
+ {% include 'secrets/inc/assigned_secrets.html' %}
{% endif %}
diff --git a/netbox/templates/secrets/inc/assigned_secrets.html b/netbox/templates/secrets/inc/assigned_secrets.html
new file mode 100644
index 000000000..11ad5e75d
--- /dev/null
+++ b/netbox/templates/secrets/inc/assigned_secrets.html
@@ -0,0 +1,41 @@
+
+
+ Secrets
+
+ {% if secrets %}
+
+ {% for secret in secrets %}
+
+ {{ secret.role }} |
+ {{ secret.name }} |
+ ******** |
+
+
+
+
+ |
+
+ {% endfor %}
+
+ {% else %}
+
+ None found
+
+ {% endif %}
+ {% if perms.secrets.add_secret %}
+
+
+ {% endif %}
+
diff --git a/netbox/templates/secrets/inc/secret_tr.html b/netbox/templates/secrets/inc/secret_tr.html
deleted file mode 100644
index 2af609289..000000000
--- a/netbox/templates/secrets/inc/secret_tr.html
+++ /dev/null
@@ -1,16 +0,0 @@
-
- {{ secret.role }} |
- {{ secret.name }} |
- ******** |
-
-
-
-
- |
-
diff --git a/netbox/templates/secrets/secret_edit.html b/netbox/templates/secrets/secret_edit.html
index 0cb1eefef..d3c2f88dc 100644
--- a/netbox/templates/secrets/secret_edit.html
+++ b/netbox/templates/secrets/secret_edit.html
@@ -18,9 +18,24 @@
{% endif %}
-
Secret Attributes
+
+ Secret Assignment
+
- {% render_field form.device %}
+ {% with vm_tab_active=form.initial.virtual_machine %}
+
+
+
+ {% render_field form.device %}
+
+
+ {% render_field form.virtual_machine %}
+
+
+ {% endwith %}
{% render_field form.role %}
{% render_field form.name %}
{% render_field form.userkeys %}
diff --git a/netbox/templates/virtualization/virtualmachine.html b/netbox/templates/virtualization/virtualmachine.html
index ea33aa460..3f0a37b88 100644
--- a/netbox/templates/virtualization/virtualmachine.html
+++ b/netbox/templates/virtualization/virtualmachine.html
@@ -220,6 +220,9 @@
+ {% if perms.secrets.view_secret %}
+ {% include 'secrets/inc/assigned_secrets.html' %}
+ {% endif %}
Services
@@ -325,8 +328,10 @@
{% endif %}
+{% include 'secrets/inc/private_key_modal.html' %}
{% endblock %}
{% block javascript %}
+
{% endblock %}
diff --git a/netbox/virtualization/models.py b/netbox/virtualization/models.py
index a492370ef..e81ee1e49 100644
--- a/netbox/virtualization/models.py
+++ b/netbox/virtualization/models.py
@@ -270,6 +270,12 @@ class VirtualMachine(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
comments = models.TextField(
blank=True
)
+ secrets = GenericRelation(
+ to='secrets.Secret',
+ content_type_field='assigned_object_type',
+ object_id_field='assigned_object_id',
+ related_query_name='virtual_machine'
+ )
tags = TaggableManager(through=TaggedItem)
objects = RestrictedQuerySet.as_manager()
diff --git a/netbox/virtualization/views.py b/netbox/virtualization/views.py
index a06a2e5ff..5e4b99553 100644
--- a/netbox/virtualization/views.py
+++ b/netbox/virtualization/views.py
@@ -9,6 +9,7 @@ from dcim.tables import DeviceTable
from extras.views import ObjectConfigContextView
from ipam.models import IPAddress, Service
from ipam.tables import InterfaceIPAddressTable, InterfaceVLANTable
+from secrets.models import Secret
from utilities.utils import get_subquery
from utilities.views import (
BulkComponentCreateView, BulkDeleteView, BulkEditView, BulkImportView, BulkRenameView, ComponentCreateView,
@@ -240,23 +241,30 @@ class VirtualMachineView(ObjectView):
queryset = VirtualMachine.objects.prefetch_related('tenant__group')
def get(self, request, pk):
-
virtualmachine = get_object_or_404(self.queryset, pk=pk)
+
+ # Interfaces
interfaces = VMInterface.objects.restrict(request.user, 'view').filter(
virtual_machine=virtualmachine
).prefetch_related(
Prefetch('ip_addresses', queryset=IPAddress.objects.restrict(request.user))
)
+
+ # Services
services = Service.objects.restrict(request.user, 'view').filter(
virtual_machine=virtualmachine
).prefetch_related(
Prefetch('ipaddresses', queryset=IPAddress.objects.restrict(request.user))
)
+ # Secrets
+ secrets = Secret.objects.restrict(request.user, 'view').filter(virtual_machine=virtualmachine)
+
return render(request, 'virtualization/virtualmachine.html', {
'virtualmachine': virtualmachine,
'interfaces': interfaces,
'services': services,
+ 'secrets': secrets,
})