diff --git a/computes/forms.py b/computes/forms.py index 9caccb82..905321c3 100644 --- a/computes/forms.py +++ b/computes/forms.py @@ -11,6 +11,12 @@ class KeepSecretsMixin: """The form never shows the stored password or gstfsd key: left empty on an edit, they stay as they are.""" + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + for name in ("password", "gstfsd_key"): + if name in self.fields and getattr(self.instance, name): + self.fields[name].widget.attrs["placeholder"] = _("Stored, leave empty to keep") + def clean_password(self): return self.cleaned_data["password"] or self.instance.password @@ -19,43 +25,62 @@ class KeepSecretsMixin: SECRET_WIDGETS = {"password": forms.PasswordInput(), "gstfsd_key": forms.PasswordInput()} +DETAILS = {"details": _("Description")} +DETAILS_HELP = {"details": _("Shown on the hosts list")} +SASL_LABELS = {"login": _("SASL login"), "password": _("SASL password")} | DETAILS class TcpComputeForm(KeepSecretsMixin, forms.ModelForm): + kind = "TCP" + about = _("libvirt over TCP, with a SASL login") hostname = forms.CharField(validators=[validate_hostname]) type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TCP) class Meta: model = Compute widgets = SECRET_WIDGETS + labels = SASL_LABELS + help_texts = DETAILS_HELP fields = "__all__" class SshComputeForm(KeepSecretsMixin, forms.ModelForm): + kind = "SSH" + about = _("libvirt over SSH, with the panel's SSH key") hostname = forms.CharField(validators=[validate_hostname], label=_("FQDN/IP")) type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SSH) class Meta: model = Compute widgets = SECRET_WIDGETS + labels = {"login": _("SSH user")} | DETAILS + help_texts = DETAILS_HELP exclude = ["password"] class TlsComputeForm(KeepSecretsMixin, forms.ModelForm): + kind = "TLS" + about = _("libvirt over TLS, with certificates") hostname = forms.CharField(validators=[validate_hostname]) type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_TLS) class Meta: model = Compute widgets = SECRET_WIDGETS + labels = SASL_LABELS + help_texts = DETAILS_HELP fields = "__all__" class SocketComputeForm(KeepSecretsMixin, forms.ModelForm): + kind = _("Local") + about = _("the libvirt of the host the panel runs on, by its socket") hostname = forms.CharField(widget=forms.HiddenInput, initial="localhost") type = forms.IntegerField(widget=forms.HiddenInput, initial=CONN_SOCKET) class Meta: model = Compute widgets = SECRET_WIDGETS + labels = DETAILS + help_texts = DETAILS_HELP fields = ["name", "details", "hostname", "type", "migration_address", "gstfsd_key"] diff --git a/computes/templates/computes/form.html b/computes/templates/computes/form.html index 24e8dd67..2b6f9713 100644 --- a/computes/templates/computes/form.html +++ b/computes/templates/computes/form.html @@ -1,29 +1,49 @@ {% extends "base.html" %} {% load django_bootstrap5 %} -{% load bootstrap_icons %} {% load i18n %} -{% block title %}{% if form.instance.pk %}{% trans "Edit Compute" %}{% else %}{% trans "Add Compute" %}{% endif %}{% endblock %} +{% block title %}{% if form.instance.pk %}{% trans "Edit host" %}{% else %}{% trans "Add host" %}{% endif %}{% endblock %} + +{% block page_heading %} + {% if form.instance.pk %}{% blocktrans with name=form.instance.name %}Edit host: {{ name }}{% endblocktrans %}{% else %}{% trans "Add host" %}{% endif %} + {{ form.kind }} +{% endblock page_heading %} {% block content %} -
-
-

{% if form.instance.pk %}{% trans "Edit Compute" %}{% else %}{% trans "Add Compute" %}{% endif %}

-
-
-
-
-
- {% csrf_token %} - {% bootstrap_form form layout='horizontal' %} -
-
- - {% bs_icon 'x-square-fill' %} {% trans "Cancel" %} - +

{{ form.about }}

+
+ {% csrf_token %} + {% bootstrap_form_errors form type='non_fields' %} + {% for field in form.hidden_fields %}{{ field }}{% endfor %} +
+

{% trans "Connection" %}

+
+
+
{% bootstrap_field form.name %}
+ {% if not form.hostname.is_hidden %}
{% bootstrap_field form.hostname %}
{% endif %} + {% if "login" in form.fields %}
{% bootstrap_field form.login %}
{% endif %} + {% if "password" in form.fields %}
{% bootstrap_field form.password %}
{% endif %} +
{% bootstrap_field form.details %}
+
+ {# open when one of them is set or refused #} +
+ + {% trans "Advanced" %} + + {% trans "Migration address" %}: {{ form.migration_address.value|default:_("the host name") }} + · {% trans "gstfsd key" %}: {% if form.instance.gstfsd_key %}{% trans "stored" %}{% else %}{% trans "none" %}{% endif %} + + +
+ {% bootstrap_field form.migration_address %} + {% bootstrap_field form.gstfsd_key %} +
+
+
+
+ {% trans "Cancel" %} +
-
-{% endblock content %} \ No newline at end of file + +{% endblock content %} diff --git a/computes/tests/test_compute_password.py b/computes/tests/test_compute_password.py index aee972eb..4ea04791 100644 --- a/computes/tests/test_compute_password.py +++ b/computes/tests/test_compute_password.py @@ -29,9 +29,23 @@ class ComputePasswordTests(TestCase): def test_a_long_password_is_kept_and_the_page_says_edit(self): compute = Compute.objects.create(name="cp-long", hostname="192.0.2.7", login="admin", password="", type=1) - self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])), "Edit Compute") + self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])), "Edit host") self.assertEqual(self.edit(compute, "p" * 40), "p" * 40) + def test_the_form_says_a_secret_is_stored_and_names_the_connection(self): + compute = Compute.objects.create(name="cp-stored", hostname="192.0.2.9", login="admin", password="secret", + type=1) + response = self.client.get(reverse("compute_update", args=[compute.id])) + self.assertContains(response, 'placeholder="Stored, leave empty to keep"', count=1) # the password only + self.assertContains(response, "libvirt over TCP, with a SASL login") + self.assertContains(response, "SASL password") + self.assertNotContains(self.client.get(reverse("add_tcp_host")), "Stored, leave empty to keep") + # the advanced fields fold away unless one is set + self.assertNotContains(response, "
") + Compute.objects.filter(pk=compute.pk).update(migration_address="192.0.2.19") + self.assertContains(self.client.get(reverse("compute_update", args=[compute.id])), + "
") + def test_the_api_keeps_the_password_when_it_is_left_empty(self): compute = Compute.objects.create(name="cp-api", hostname="192.0.2.8", login="admin", password="secret", type=1) for password in ("", None):