Skip to content

Commit

Permalink
Merge pull request #2699 from lunkwill42/feature/snmpv3-management-pr…
Browse files Browse the repository at this point in the history
…ofile

Add an SNMPv3 management profile type
  • Loading branch information
lunkwill42 authored Nov 13, 2023
2 parents 7d71130 + bca69ae commit 4d9de66
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 4 deletions.
17 changes: 13 additions & 4 deletions python/nav/models/manage.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,11 @@ class ManagementProfile(models.Model):
PROTOCOL_DEBUG = 0
PROTOCOL_SNMP = 1
PROTOCOL_NAPALM = 2
PROTOCOL_SNMPV3 = 3
PROTOCOL_CHOICES = [
(PROTOCOL_SNMP, "SNMP"),
(PROTOCOL_NAPALM, "NAPALM"),
(PROTOCOL_SNMPV3, "SNMPv3"),
]
if settings.DEBUG:
PROTOCOL_CHOICES.insert(0, (PROTOCOL_DEBUG, 'debug'))
Expand All @@ -152,16 +154,18 @@ def __str__(self):

@property
def is_snmp(self):
return self.protocol == self.PROTOCOL_SNMP
return self.protocol in (self.PROTOCOL_SNMP, self.PROTOCOL_SNMPV3)

@property
def snmp_version(self):
"""Returns the configured SNMP version as an integer"""
if self.is_snmp:
if self.protocol == self.PROTOCOL_SNMP:
value = self.configuration['version']
if value == "2c":
return 2
return int(value)
elif self.protocol == self.PROTOCOL_SNMPV3:
return 3

raise ValueError(
"Getting snmp protocol version for non-snmp management profile"
Expand Down Expand Up @@ -359,7 +363,12 @@ def get_preferred_snmp_management_profile(self, writeable=None):
Returns the snmp management profile with the highest available
SNMP version.
"""
query = Q(protocol=ManagementProfile.PROTOCOL_SNMP)
query = Q(
protocol__in=(
ManagementProfile.PROTOCOL_SNMP,
ManagementProfile.PROTOCOL_SNMPV3,
)
)
if writeable:
query = query & Q(configuration__write=True)
elif writeable is not None:
Expand All @@ -368,7 +377,7 @@ def get_preferred_snmp_management_profile(self, writeable=None):
)
profiles = sorted(
self.profiles.filter(query),
key=lambda p: str(p.configuration.get('version') or 0),
key=lambda p: p.snmp_version or 0,
reverse=True,
)
if profiles:
Expand Down
100 changes: 100 additions & 0 deletions python/nav/web/seeddb/page/management_profile/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,106 @@ class Meta(object):
)


class SnmpV3Form(ProtocolSpecificMixIn, forms.ModelForm):
PROTOCOL = ManagementProfile.PROTOCOL_SNMPV3
PROTOCOL_NAME = PROTOCOL_CHOICES.get(PROTOCOL)
NOTABENE = "SNMPv3 is not yet fully supported in NAV"

class Meta(object):
model = ManagementProfile
configuration_fields = [
"sec_level",
"auth_protocol",
"sec_name",
"auth_password",
"priv_protocol",
"priv_password",
"write",
]
fields = []

sec_level = forms.ChoiceField(
label="Security level",
choices=(
("noAuthNoPriv", "noAuthNoPriv"),
("authNoPriv", "authNoPriv"),
("authPriv", "authPriv"),
),
help_text="The required SNMPv3 security level",
)
auth_protocol = forms.ChoiceField(
label="Authentication protocol",
choices=(
("MD5", "MD5"),
("SHA", "SHA"),
("SHA-512", "SHA-512"),
("SHA-384", "SHA-384"),
("SHA-256", "SHA-256"),
("SHA-224", "SHA-224"),
),
help_text="Authentication protocol to use",
)
sec_name = forms.CharField(
widget=forms.TextInput(attrs={"autocomplete": "off"}),
label="Security name",
help_text=(
"The username to authenticate as. This is required even if noAuthPriv "
"security mode is selected."
),
)
auth_password = forms.CharField(
widget=forms.PasswordInput(render_value=True, attrs={"autocomplete": "off"}),
label="Authentication password",
help_text=(
"The password to authenticate the user. Required for authNoPriv or "
"authPriv security levels."
),
required=False,
)
priv_protocol = forms.ChoiceField(
label="Privacy protocol",
choices=(
("DES", "DES"),
("AES", "AES"),
),
help_text="Privacy protocol to use. Required for authPriv security level.",
required=False,
)
priv_password = forms.CharField(
widget=forms.PasswordInput(render_value=True, attrs={"autocomplete": "off"}),
label="Privacy password",
help_text=(
"The password to use for DES or AES encryption. Required for authPriv "
"security level."
),
required=False,
)
write = forms.BooleanField(
initial=False,
required=False,
label="Enables write access",
help_text="Check this if this profile enables write access",
)

def clean_auth_password(self):
level = self.cleaned_data.get("sec_level")
password = self.cleaned_data.get("auth_password").strip()
if level.startswith("auth") and not password:
raise forms.ValidationError(
f"Authentication password must be set for security level {level}"
)
return password

def clean_priv_password(self):
level = self.cleaned_data.get("sec_level")
password = self.cleaned_data.get("priv_password").strip()
if level == "authPriv" and not password:
raise forms.ValidationError(
f"Privacy password must be set for security level {level}"
)
return password


class NapalmForm(ProtocolSpecificMixIn, forms.ModelForm):
PROTOCOL = ManagementProfile.PROTOCOL_NAPALM
PROTOCOL_NAME = PROTOCOL_CHOICES.get(PROTOCOL)
Expand Down
3 changes: 3 additions & 0 deletions python/nav/web/templates/seeddb/management-profile/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ <h4>Add new management profile</h4>
{% for form in protocol_forms %}
<fieldset class="protocol-configuration" id="protocol-{{ form.PROTOCOL }}">
<legend>{{ form.PROTOCOL_NAME }} configuration</legend>
{% if form.NOTABENE %}
<div class="alert-box warning with-icon">{{ form.NOTABENE }}</div>
{% endif %}
{{ form | crispy }}
</fieldset>
{% endfor %}
Expand Down
48 changes: 48 additions & 0 deletions tests/unittests/seeddb/management_profile_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from nav.web.seeddb.page.management_profile.forms import SnmpV3Form


class TestSnmpv3Form:
def test_when_seclevel_is_noauth_then_it_should_not_require_auth_password(self):
form = SnmpV3Form(
dict(
sec_level="noAuthNoPriv",
auth_protocol="MD5",
sec_name="foo",
auth_password="",
)
)
assert form.is_valid()

def test_when_seclevel_is_auth_then_it_should_require_auth_password(self):
form = SnmpV3Form(
dict(
sec_level="authNoPriv",
auth_protocol="MD5",
sec_name="foo",
auth_password="",
)
)
assert not form.is_valid()

def test_when_seclevel_is_priv_then_it_should_require_priv_password(self):
form = SnmpV3Form(
dict(
sec_level="authPriv",
auth_protocol="MD5",
sec_name="foo",
auth_password="bar",
)
)
assert not form.is_valid()

def test_when_seclevel_is_priv_then_it_should_accept_priv_password(self):
form = SnmpV3Form(
dict(
sec_level="authPriv",
auth_protocol="MD5",
sec_name="foo",
auth_password="bar",
priv_password="cromulent",
)
)
assert form.is_valid()

0 comments on commit 4d9de66

Please sign in to comment.