diff --git a/computes/templates/computes/pool_bar.html b/computes/templates/computes/pool_bar.html index ad873d01..1f63458e 100644 --- a/computes/templates/computes/pool_bar.html +++ b/computes/templates/computes/pool_bar.html @@ -1,5 +1,6 @@ {% load i18n %} -
{{ pool.name }}
+{# a pool's space; hide_name: the page names the pool next to it #} +{% if not hide_name %}
{{ pool.name }}
{% endif %}
diff --git a/computes/tests/test_overview.py b/computes/tests/test_overview.py index 3aaa67fe..31daac03 100644 --- a/computes/tests/test_overview.py +++ b/computes/tests/test_overview.py @@ -18,7 +18,8 @@ GIVEN = {"cpus": 8, "memory": 32 * GIB, "vcpus": {"all": 12, "running": 6}, def pool(name, size_gib, free_gib, status=1): - return {"name": name, "status": status, "size": size_gib * GIB, "free": free_gib * GIB} + return {"name": name, "status": status, "size": size_gib * GIB, "free": free_gib * GIB, + "used_percent": (size_gib - free_gib) * 100 / size_gib} def text(page, xpath): diff --git a/computes/views.py b/computes/views.py index 12d3c24b..3706456e 100644 --- a/computes/views.py +++ b/computes/views.py @@ -64,8 +64,6 @@ def host_pools(compute): except libvirtError: return None pools.sort(key=lambda pool: (pool["name"] != "default", pool["name"])) - for pool in pools: - pool["used_percent"] = (pool["size"] - pool["free"]) * 100 / pool["size"] if pool["size"] else 0 return {"pool": pools[0] if pools else None, "more_pools": pools[1:]} diff --git a/instances/templates/create_instance_w2.html b/instances/templates/create_instance_w2.html index 7f35cc87..23f066b6 100644 --- a/instances/templates/create_instance_w2.html +++ b/instances/templates/create_instance_w2.html @@ -13,10 +13,6 @@ {% endblock %} diff --git a/static/css/webvirtcloud.css b/static/css/webvirtcloud.css index 01855f3d..7c1265df 100644 --- a/static/css/webvirtcloud.css +++ b/static/css/webvirtcloud.css @@ -65,6 +65,23 @@ th[aria-sort="ascending"]:not([data-sort-method="none"])::after { } } +/* a choice shown as a card around its radio button (the VM's disk source, + a new pool's type): the chosen card by its border only, as the + Bootswatch dark themes keep light "subtle" backgrounds */ +.source-option { + cursor: pointer; +} + +.source-option:has(input:checked) { + border-color: var(--bs-primary) !important; + box-shadow: inset 0 0 0 1px var(--bs-primary); +} + +.source-option:has(input:focus-visible) { + outline: 2px solid var(--bs-primary); + outline-offset: 2px; +} + .host-collapsed tr[host] { display: none; } diff --git a/static/icon_cache/toggle-off_None_None_None.svg b/static/icon_cache/toggle-off_None_None_None.svg new file mode 100644 index 00000000..b54ea042 --- /dev/null +++ b/static/icon_cache/toggle-off_None_None_None.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/icon_cache/toggle-on_None_None_None.svg b/static/icon_cache/toggle-on_None_None_None.svg new file mode 100644 index 00000000..9f9cb44e --- /dev/null +++ b/static/icon_cache/toggle-on_None_None_None.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/icon_cache/upload_None_None_None.svg b/static/icon_cache/upload_None_None_None.svg new file mode 100644 index 00000000..603bce64 --- /dev/null +++ b/static/icon_cache/upload_None_None_None.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/storages/forms.py b/storages/forms.py index 05e10729..f6692812 100644 --- a/storages/forms.py +++ b/storages/forms.py @@ -11,61 +11,79 @@ POOL_PATH = re.compile(r"(?!-)(?:[/0-9a-zA-Z_.:]|(? 20: - raise forms.ValidationError( - _("The pool name must not exceed 20 characters") - ) + if any(pool["name"] == name for pool in self.pools): + raise forms.ValidationError(_("This host has a pool of that name"), code="taken") return name def clean_target(self): - storage_type = self.cleaned_data["stg_type"] target = self.cleaned_data["target"] - have_symbol = POOL_PATH.fullmatch(target) - if storage_type in ["dir", "netfs"] and not have_symbol: + if target and not POOL_PATH.fullmatch(target): raise forms.ValidationError( _("The target must not contain any special characters") ) - if storage_type in ["dir", "netfs"] and not target: - raise forms.ValidationError(_("No path has been entered")) + # an RBD pool's path is its Ceph pool, not a directory + used = next((pool["name"] for pool in self.pools + if pool["type"] != "rbd" and pool["path"] and pool["path"].rstrip("/") == target.rstrip("/")), None) + if target and used: + raise forms.ValidationError(_("Pool %(pool)s uses this path") % {"pool": used}) return target def clean_source(self): - storage_type = self.cleaned_data["stg_type"] source = self.cleaned_data["source"] - have_symbol = POOL_PATH.fullmatch(source) - if storage_type in ["logical", "netfs"]: - if not source: - raise forms.ValidationError(_("No device or path has been entered")) - if not have_symbol: - raise forms.ValidationError( - _("The disk source must not contain any special characters") - ) + if source and not POOL_PATH.fullmatch(source): + raise forms.ValidationError( + _("The disk source must not contain any special characters") + ) return source + def clean(self): + data = super().clean() + for field in self.NEEDS.get(data.get("stg_type"), ()): + if not data.get(field) and field not in self.errors: + self.add_error(field, _("This field is required.")) + if data.get("stg_type") == "rbd" and data.get("secret") and data["secret"] not in self.ceph_secrets: + self.add_error("secret", _("Choose one of the host's Ceph secrets")) + return data + class CreateVolumeForm(forms.Form): name = forms.CharField(max_length=120) diff --git a/storages/templates/create_stg_block.html b/storages/templates/create_stg_block.html index 51658437..71347b86 100644 --- a/storages/templates/create_stg_block.html +++ b/storages/templates/create_stg_block.html @@ -1,258 +1,248 @@ {% load i18n %} {% load bootstrap_icons %} +{% load form_fields %} {% if request.user.is_superuser %} - - - + - -