diff --git a/charms/glance-k8s/config.yaml b/charms/glance-k8s/config.yaml index 8631c2d7..541c7313 100644 --- a/charms/glance-k8s/config.yaml +++ b/charms/glance-k8s/config.yaml @@ -259,3 +259,14 @@ options: type: boolean default: False description: Enable notifications to send to telemetry. + image-size-cap: + type: string + default: 5GB + description: | + Maximum size of image a user can upload. Defaults to 5GB + (5368709120 bytes). Example values: 500M, 500MB, 5G, 5TB. + Valid units: K, KB, M, MB, G, GB, T, TB, P, PB. If no units provided, + bytes are assumed. + . + WARNING: this value should only be increased after careful consideration + and must be set to a value under 8EB (9223372036854775808 bytes). diff --git a/charms/glance-k8s/metadata.yaml b/charms/glance-k8s/metadata.yaml index f038523f..782052a8 100644 --- a/charms/glance-k8s/metadata.yaml +++ b/charms/glance-k8s/metadata.yaml @@ -42,6 +42,7 @@ resources: storage: local-repository: type: filesystem + minimum-size: 10GiB description: | A local filesystem storage repository for glance images to be saved to. Note, this must be shared storage in order to support a highly diff --git a/charms/glance-k8s/src/charm.py b/charms/glance-k8s/src/charm.py index 1f42b7f0..c34a7a10 100755 --- a/charms/glance-k8s/src/charm.py +++ b/charms/glance-k8s/src/charm.py @@ -22,6 +22,7 @@ This charm provide Glance services as part of an OpenStack deployment """ import logging +import re from typing import ( Callable, List, @@ -32,7 +33,18 @@ import ops_sunbeam.compound_status as compound_status import ops_sunbeam.config_contexts as sunbeam_ctxts import ops_sunbeam.container_handlers as sunbeam_chandlers import ops_sunbeam.core as sunbeam_core +import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers +from lightkube.core.client import ( + Client, +) +from lightkube.core.exceptions import ( + ApiError, +) +from lightkube.resources.core_v1 import ( + PersistentVolumeClaim, + Pod, +) from ops.charm import ( CharmBase, ) @@ -51,6 +63,7 @@ from ops.model import ( logger = logging.getLogger(__name__) IMAGES_DIR = "/var/lib/glance/images" +STORAGE_NAME = "local-repository" # Use Apache to translate / to /. This should be possible # adding rules to the api-paste.ini but this does not seem to work @@ -186,6 +199,58 @@ class GlanceStorageRelationHandler(sunbeam_rhandlers.CephClientHandler): return {} +class GlanceConfigContext(sunbeam_ctxts.ConfigContext): + """Glance configuration context.""" + + def __init__( + self, + charm: sunbeam_charm.OSBaseOperatorAPICharm, + namespace: str, + ) -> None: + """Initialise the context.""" + super().__init__(charm, namespace) + + def context(self) -> dict: + """Context used when rendering templates.""" + return { + "image_size_cap": bytes_from_string( + self.charm.config["image-size-cap"] + ), + } + + +def bytes_from_string(value: str) -> int: + """Interpret human readable string value as bytes. + + Returns int + """ + byte_power = { + "K": 1, + "KB": 1, + "Ki": 1, + "M": 2, + "MB": 2, + "Mi": 2, + "G": 3, + "GB": 3, + "Gi": 3, + "T": 4, + "TB": 4, + "Ti": 4, + "P": 5, + "PB": 5, + "Pi": 5, + } + matches = re.match(r"([0-9]+)\s*([a-zA-Z]+)", value) + if matches: + return int(matches.group(1)) * (1024 ** byte_power[matches.group(2)]) + try: + return int(value) + except ValueError: + msg = f"Unable to interpret string value {value!r} as bytes" + raise ValueError(msg) + + class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Charm the service.""" @@ -243,6 +308,7 @@ class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): self, "cinder_ceph" ) ) + contexts.append(GlanceConfigContext(self, "glance_config")) return contexts @property @@ -447,6 +513,75 @@ class GlanceOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): logger.info("CONFIG changed and ceph ready: calling request pools") self.ceph.request_pools(event) + def configure_unit(self, event: EventBase) -> None: + """Run configuration on this unit.""" + self.check_configuration(event) + return super().configure_unit(event) + + def check_configuration(self, event: EventBase): + """Check a configuration key is correct.""" + try: + self._validate_image_size_cap() + except ValueError as e: + raise sunbeam_guard.BlockedExceptionError(str(e)) from e + + def _validate_image_size_cap(self): + """Check image size is valid.""" + try: + image_cap_size = bytes_from_string(self.config["image-size-cap"]) + except ValueError as e: + raise ValueError( + "image-size-cap must be a number or a number followed by " + "KG, MG, GB, TB, or PB" + ) from e + if self.has_ceph_relation(): + logger.debug("ceph relation exists, skipping PVC size check") + return + pvc_size = self._fetch_volume_size() + if pvc_size < image_cap_size: + raise ValueError( + "image-size-cap must be less than the size" + " of the local-repository volume" + ) + + def _fetch_volume_size( + self, + ): + """Fetch the size of the local-repository volume.""" + client = Client() # type: ignore + try: + pod = client.get( + Pod, + name="-".join(self.unit.name.rsplit("/", 1)), + namespace=self.model.name, + ) + except ApiError as e: + if e.status.code == 404: + raise Exception("Failed to find associated pod") + raise sunbeam_guard.BlockedExceptionError(e.status.message) from e + lr_volume = None + for volume in pod.spec.volumes: + if volume.name.startswith(self.app.name + "-" + STORAGE_NAME): + lr_volume = volume + break + if lr_volume is None: + raise sunbeam_guard.BlockedExceptionError( + "Failed to find local-repository volume in pod spec" + ) + claim_name = lr_volume.persistentVolumeClaim.claimName + + try: + pvc = client.get( + PersistentVolumeClaim, + name=claim_name, + namespace=self.model.name, + ) + except ApiError as e: + if e.status.code == 404: + raise Exception("Failed to find associated PVC") + raise sunbeam_guard.BlockedExceptionError(e.status.message) from e + return bytes_from_string(pvc.status.capacity["storage"]) + if __name__ == "__main__": main(GlanceOperatorCharm) diff --git a/charms/glance-k8s/src/templates/glance-api.conf.j2 b/charms/glance-k8s/src/templates/glance-api.conf.j2 index ac1de568..c20cd98d 100644 --- a/charms/glance-k8s/src/templates/glance-api.conf.j2 +++ b/charms/glance-k8s/src/templates/glance-api.conf.j2 @@ -10,6 +10,7 @@ transport_url = {{ amqp.transport_url }} {% endif %} bind_port = 9282 workers = 4 +image_size_cap = {{ glance_config.image_size_cap }} {% if ceph.auth %} enabled_backends = filestore:file, ceph:rbd diff --git a/tests/caas/smoke.yaml.j2 b/tests/caas/smoke.yaml.j2 index 24780ae5..789c676b 100644 --- a/tests/caas/smoke.yaml.j2 +++ b/tests/caas/smoke.yaml.j2 @@ -80,7 +80,7 @@ applications: scale: 1 trust: true storage: - local-repository: 5G + local-repository: 10G resources: glance-api-image: ghcr.io/canonical/glance-api:2024.1 heat: diff --git a/tests/core/smoke.yaml.j2 b/tests/core/smoke.yaml.j2 index 915c7f5f..0fe710ff 100644 --- a/tests/core/smoke.yaml.j2 +++ b/tests/core/smoke.yaml.j2 @@ -85,7 +85,7 @@ applications: scale: 1 trust: true storage: - local-repository: 5G + local-repository: 10G resources: glance-api-image: ghcr.io/canonical/glance-api:2024.1 nova: diff --git a/tests/tempest/smoke.yaml.j2 b/tests/tempest/smoke.yaml.j2 index 75e6f151..962f6684 100644 --- a/tests/tempest/smoke.yaml.j2 +++ b/tests/tempest/smoke.yaml.j2 @@ -85,7 +85,7 @@ applications: scale: 1 trust: true storage: - local-repository: 5G + local-repository: 10G resources: glance-api-image: ghcr.io/canonical/glance-api:2024.1 nova: