From 77067e7b4c4b8f62c51919460363bf2c7e28b204 Mon Sep 17 00:00:00 2001 From: Hemanth Nakkina Date: Mon, 6 May 2024 13:38:51 +0530 Subject: [PATCH] Support external dns for cloud guests Add interface designate to share the dns endpoint. Update designate-k8s to implement provides side of the interface and add endpoint data to the relation app databag. Update neutron-k8s to implement requires side of the interface. Add new options reverse-dns-lookup, ipv4-ptr-zone-prefix-size, ipv6-ptr-zone-prefix-size. Update neutron conf templates to add external dns related configuration. Change-Id: Ie7a481c7b90583981e7d68f6a54dfb0e6f1796dd --- charms/designate-k8s/metadata.yaml | 4 + charms/designate-k8s/src/charm.py | 77 +++++++ charms/neutron-k8s/config.yaml | 26 +++ charms/neutron-k8s/metadata.yaml | 3 + charms/neutron-k8s/src/charm.py | 114 ++++++++++ .../neutron-k8s/src/templates/neutron.conf.j2 | 23 +++ common.sh | 2 + .../designate_k8s/v0/designate_service.py | 195 ++++++++++++++++++ 8 files changed, 444 insertions(+) create mode 100644 libs/internal/lib/charms/designate_k8s/v0/designate_service.py diff --git a/charms/designate-k8s/metadata.yaml b/charms/designate-k8s/metadata.yaml index 88dd5c4b..4c7a0a5c 100644 --- a/charms/designate-k8s/metadata.yaml +++ b/charms/designate-k8s/metadata.yaml @@ -27,6 +27,10 @@ resources: description: OCI image for OpenStack designate upstream-source: ghcr.io/canonical/designate-consolidated:2024.1 +provides: + dnsaas: + interface: designate + requires: database: interface: mysql_client diff --git a/charms/designate-k8s/src/charm.py b/charms/designate-k8s/src/charm.py index 96a01f4b..4dcf96a8 100755 --- a/charms/designate-k8s/src/charm.py +++ b/charms/designate-k8s/src/charm.py @@ -40,6 +40,10 @@ import ops_sunbeam.core as sunbeam_core import ops_sunbeam.guard as sunbeam_guard import ops_sunbeam.relation_handlers as sunbeam_rhandlers import tenacity +from charms.designate_k8s.v0.designate_service import ( + DesignateEndpointRequestEvent, + DesignateServiceProvides, +) from ops.main import ( main, ) @@ -171,6 +175,42 @@ class DesignatePebbleHandler(sunbeam_chandlers.WSGIPebbleHandler): super().init_service(context) +class DesignateServiceProvidesHandler(sunbeam_rhandlers.RelationHandler): + """Handler for designate service relation.""" + + def __init__( + self, + charm: ops.CharmBase, + relation_name: str, + callback_f: Callable, + ): + super().__init__(charm, relation_name, callback_f) + + def setup_event_handler(self): + """Configure event handlers for an Ceilometer service relation.""" + logger.debug("Setting up Ceilometer service event handler") + svc = DesignateServiceProvides( + self.charm, + self.relation_name, + ) + self.framework.observe( + svc.on.endpoint_request, + self._on_endpoint_request, + ) + return svc + + def _on_endpoint_request( + self, event: DesignateEndpointRequestEvent + ) -> None: + """Handle endpoint request event.""" + self.callback_f(event) + + @property + def ready(self) -> bool: + """Report if relation is ready.""" + return True + + class BindRndcRequiresRelationHandler(sunbeam_rhandlers.RelationHandler): """Relation handler class.""" @@ -371,6 +411,13 @@ class DesignateOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): ) -> List[sunbeam_rhandlers.RelationHandler]: """Relation handlers for the service.""" handlers = handlers or [] + if self.can_add_handler("dnsaas", handlers): + self.dnsaas = DesignateServiceProvidesHandler( + self, + "dnsaas", + self.set_dns_endpoint_from_event, + ) + handlers.append(self.dnsaas) if self.can_add_handler(BIND_RNDC_RELATION, handlers): self.bind_rndc = BindRndcRequiresRelationHandler( self, @@ -481,6 +528,36 @@ class DesignateOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): except ops.SecretNotFoundError: return None + def _ingress_changed(self, event: ops.framework.EventBase) -> None: + """Ingress changed callback. + + Invoked when the data on the ingress relation has changed. This will + update the relevant endpoints with the identity service, and then + call the configure_charm. + """ + self.set_dns_endpoint_on_update() + super()._ingress_changed(event) + + def set_dns_endpoint_from_event( + self, event: ops.framework.EventBase + ) -> None: + """Set endpoint in relation data.""" + if self.internal_url: + self.dnsaas.interface.set_endpoint( + relation=event.relation, endpoint=self.internal_url + ) + else: + logging.debug("DNS Endpoint not yet set, not sending config") + + def set_dns_endpoint_on_update(self) -> None: + """Set endpoint on relation on update of local data.""" + if self.internal_url: + self.dnsaas.interface.set_endpoint( + relation=None, endpoint=self.internal_url + ) + else: + logging.debug("DNS Endpoint not yet set, not sending config") + if __name__ == "__main__": main(DesignateOperatorCharm) diff --git a/charms/neutron-k8s/config.yaml b/charms/neutron-k8s/config.yaml index f52c3e7d..107f653f 100644 --- a/charms/neutron-k8s/config.yaml +++ b/charms/neutron-k8s/config.yaml @@ -73,3 +73,29 @@ options: . Use this if a subset of your flat or VLAN provider networks have a MTU that differ with what is set in global-physnet-mtu. + reverse-dns-lookup: + default: False + description: | + A boolean value specifying whether to enable or not the creation of + reverse lookup (PTR) records. + . + NOTE: Use only when integrating neutron-k8s charm to designate charm. + type: boolean + ipv4-ptr-zone-prefix-size: + default: 24 + description: | + The size in bits of the prefix for the IPv4 reverse lookup (PTR) zones. + Valid size has to be multiple of 8, with maximum value of 24 and minimum + value of 8. + . + NOTE: Use only when "reverse-dns-lookup" option is set to "True". + type: int + ipv6-ptr-zone-prefix-size: + default: 64 + description: | + The size in bits of the prefix for the IPv6 reverse lookup (PTR) zones. + Valid size has to be multiple of 4, with maximum value of 124 and minimum + value of 4. + . + NOTE: Use only when "reverse-dns-lookup" option is set to "True". + type: int diff --git a/charms/neutron-k8s/metadata.yaml b/charms/neutron-k8s/metadata.yaml index a5505cb3..461a3794 100644 --- a/charms/neutron-k8s/metadata.yaml +++ b/charms/neutron-k8s/metadata.yaml @@ -60,6 +60,9 @@ requires: receive-ca-cert: interface: certificate_transfer optional: true + external-dns: + interface: designate + optional: true peers: peers: diff --git a/charms/neutron-k8s/src/charm.py b/charms/neutron-k8s/src/charm.py index b4809720..08341f31 100755 --- a/charms/neutron-k8s/src/charm.py +++ b/charms/neutron-k8s/src/charm.py @@ -21,7 +21,12 @@ This charm provide Neutron services as part of an OpenStack deployment import logging import re +from typing import ( + Callable, + List, +) +import charms.designate_k8s.v0.designate_service as designate_svc import ops import ops_sunbeam.charm as sunbeam_charm import ops_sunbeam.config_contexts as sunbeam_ctxts @@ -37,10 +42,81 @@ from ops.framework import ( from ops.main import ( main, ) +from ops.model import ( + BlockedStatus, +) logger = logging.getLogger(__name__) +class DesignateServiceRequiresHandler(sunbeam_rhandlers.RelationHandler): + """Handle external-dns relation on the requires side.""" + + def __init__( + self, + charm: ops.charm.CharmBase, + relation_name: str, + callback_f: Callable, + mandatory: bool = False, + ): + """Create a new external-dns handler. + + Create a new DesignateServiceRequiresHandler that handles initial + events from the relation and invokes the provided callbacks based on + the event raised. + + :param charm: the Charm class the handler is for + :type charm: ops.charm.CharmBase + :param relation_name: the relation the handler is bound to + :type relation_name: str + :param callback_f: the function to call when the nodes are connected + :type callback_f: Callable + :param mandatory: If the relation is mandatory to proceed with + configuring charm + :type mandatory: bool + """ + super().__init__(charm, relation_name, callback_f, mandatory) + + def setup_event_handler(self) -> None: + """Configure event handlers for external-dns service relation.""" + logger.debug("Setting up Designate service event handler") + svc = designate_svc.DesignateServiceRequires( + self.charm, + self.relation_name, + ) + self.framework.observe( + svc.on.endpoint_changed, + self._on_endpoint_changed, + ) + self.framework.observe( + svc.on.goneaway, + self._on_goneaway, + ) + return svc + + def _on_endpoint_changed(self, event: ops.framework.EventBase) -> None: + """Handle endpoint_changed event.""" + logger.debug( + "Designate service provider endpoint changed event received" + ) + self.callback_f(event) + + def _on_goneaway(self, event: ops.framework.EventBase) -> None: + """Handle gone_away event.""" + logger.debug("Designate service relation is departed/broken") + self.callback_f(event) + if self.mandatory: + self.status.set(BlockedStatus("integration missing")) + + @property + def ready(self) -> bool: + """Whether handler is ready for use.""" + try: + return bool(self.interface.endpoint) + except (AttributeError, KeyError): + return False + + class NeutronServerPebbleHandler(sunbeam_chandlers.ServicePebbleHandler): """Handler for interacting with pebble data.""" @@ -127,6 +203,7 @@ class NeutronOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): """Check a configuration key is correct.""" try: self._validate_domain() + self._validate_ptr_zone_prefix_size() except ValueError as e: raise sunbeam_guard.BlockedExceptionError(str(e)) from e @@ -175,11 +252,48 @@ class NeutronOperatorCharm(sunbeam_charm.OSBaseOperatorAPICharm): " and hyphens (-)" ) + def _validate_ptr_zone_prefix_size(self): + """Check given ptr zone prefix size is valid.""" + ipv4_prefix_size = self.config.get("ipv4-ptr-zone-prefix-size") + valid_ipv4_prefix_size = (8 <= ipv4_prefix_size <= 24) and ( + ipv4_prefix_size % 8 + ) == 0 + if not valid_ipv4_prefix_size: + raise ValueError( + "Invalid ipv4-ptr-zone-prefix-size. Value should be between 8 - 24 and multiple of 8" + ) + + ipv6_prefix_size = self.config.get("ipv6-ptr-zone-prefix-size") + valid_ipv6_prefix_size = (4 <= ipv6_prefix_size <= 124) and ( + ipv6_prefix_size % 4 + ) == 0 + if not valid_ipv6_prefix_size: + raise ValueError( + "Invalid ipv6-ptr-zone-prefix-size. Value should be between 4 - 124 and multiple of 4" + ) + def configure_unit(self, event: ops.EventBase) -> None: """Run configuration on this unit.""" self.check_configuration(event) return super().configure_unit(event) + def get_relation_handlers( + self, handlers: List[sunbeam_rhandlers.RelationHandler] = None + ) -> List[sunbeam_rhandlers.RelationHandler]: + """Relation handlers for the service.""" + handlers = handlers or [] + if self.can_add_handler("external-dns", handlers): + self.external_dns = DesignateServiceRequiresHandler( + self, + "external-dns", + self.configure_charm, + "external-dns" in self.mandatory_relations, + ) + handlers.append(self.external_dns) + + handlers = super().get_relation_handlers(handlers) + return handlers + def get_pebble_handlers(self) -> list[sunbeam_chandlers.PebbleHandler]: """Pebble handlers for the service.""" return [ diff --git a/charms/neutron-k8s/src/templates/neutron.conf.j2 b/charms/neutron-k8s/src/templates/neutron.conf.j2 index b589ba68..3520831d 100644 --- a/charms/neutron-k8s/src/templates/neutron.conf.j2 +++ b/charms/neutron-k8s/src/templates/neutron.conf.j2 @@ -28,6 +28,10 @@ global_physnet_mtu = {{ options.global_physnet_mtu }} transport_url = {{ amqp.transport_url }} +{% if external_dns and external_dns.endpoint -%} +external_dns_driver = designate +{% endif -%} + [oslo_concurrency] lock_path = $state_path/lock @@ -69,3 +73,22 @@ username = {{ identity_service.service_user_name }} password = {{ identity_service.service_password }} {% include "parts/section-oslo-messaging-rabbit" %} + + +{% if external_dns and external_dns.endpoint -%} +[designate] +url = {{ external_dns.endpoint }} +auth_type = password +auth_url = {{ identity_service.admin_auth_url }} +project_domain_name = {{ identity_service.service_domain_name }} +user_domain_name = {{ identity_service.service_domain_name }} +project_name = {{ identity_service.service_project_name }} +username = {{ identity_service.service_user_name }} +password = {{ identity_service.service_password }} +allow_reverse_dns_lookup = {{ options.reverse_dns_lookup }} +ipv4_ptr_zone_prefix_size = {{ options.ipv4_ptr_zone_prefix_size }} +ipv6_ptr_zone_prefix_size = {{ options.ipv6_ptr_zone_prefix_size }} +{% if receive_ca_cert and receive_ca_cert.ca_bundle -%} +cafile = /usr/local/share/ca-certificates/ca-bundle.pem +{% endif -%} +{% endif -%} diff --git a/common.sh b/common.sh index 0d41724c..2587471e 100644 --- a/common.sh +++ b/common.sh @@ -36,6 +36,7 @@ INTERNAL_CINDER_CEPH_LIBS=( INTERNAL_DESIGNATE_LIBS=( "keystone_k8s" "designate_bind_k8s" + "designate_k8s" ) INTERNAL_DESIGNATE_BIND_LIBS=( @@ -54,6 +55,7 @@ INTERNAL_KEYSTONE_LIBS=( INTERNAL_NEUTRON_LIBS=( "keystone_k8s" "ovn_central_k8s" + "designate_k8s" ) INTERNAL_NOVA_LIBS=( diff --git a/libs/internal/lib/charms/designate_k8s/v0/designate_service.py b/libs/internal/lib/charms/designate_k8s/v0/designate_service.py new file mode 100644 index 00000000..8bc31eb1 --- /dev/null +++ b/libs/internal/lib/charms/designate_k8s/v0/designate_service.py @@ -0,0 +1,195 @@ +# Copyright 2024 Canonical Ltd. +# See LICENSE file for licensing details. +"""DesignateServiceProvides and Requires module. + +This library contains the Requires and Provides classes for handling +the designate interface. + +Import `DesignateServiceRequires` in your charm, with the charm object and the +relation name: + - self + - "designate" + +Two events are also available to respond to: + - endpoint_changed + - goneaway + +A basic example showing the usage of this relation follows: + +``` +from charms.designate_k8s.v0.designate_service import ( + DesignateServiceRequires +) + +class DesignateServiceClientCharm(CharmBase): + def __init__(self, *args): + super().__init__(*args) + # DesignateService Requires + self.designate_service = DesignateServiceRequires( + self, "designate", + ) + self.framework.observe( + self.designate_service.on.endpoint_changed, + self._on_designate_service_endpoint_changed + ) + self.framework.observe( + self.designate_service.on.goneaway, + self._on_designate_service_goneaway + ) + + def _on_designate_service_endpoint_changed(self, event): + '''React to the Designate service endpoint changed event. + + This event happens when DesignateService relation is added to the + model and relation data is changed. + ''' + # Do something with the configuration provided by relation. + pass + + def _on_designate_service_goneaway(self, event): + '''React to the DesignateService goneaway event. + + This event happens when DesignateService relation is removed. + ''' + # DesignateService Relation has goneaway. + pass +``` +""" + +import logging + +import ops + +logger = logging.getLogger(__name__) + +# The unique Charmhub library identifier, never change it +LIBID = "3e0a3ac75f6d46a4ac5e144bbeb357e0" + +# Increment this major API version when introducing breaking changes +LIBAPI = 0 + +# Increment this PATCH version before using `charmcraft publish-lib` or reset +# to 0 if you are raising the major API version +LIBPATCH = 1 + + +class DesignateEndpointRequestEvent(ops.RelationEvent): + """DesignateEndpointRequest Event.""" + + pass + + +class DesignateServiceProviderEvents(ops.ObjectEvents): + """Events class for `on`.""" + + endpoint_request = ops.EventSource(DesignateEndpointRequestEvent) + + +class DesignateServiceProvides(ops.Object): + """Class to be instantiated by the providing side of the relation.""" + + on = DesignateServiceProviderEvents() + + def __init__(self, charm: ops.CharmBase, relation_name: str): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_relation_changed, + ) + + def _on_relation_changed(self, event: ops.RelationChangedEvent): + self.on.endpoint_request.emit(event.relation) + + def set_endpoint( + self, relation: ops.Relation | None, endpoint: str + ) -> None: + """Set designate endpoint on the relation.""" + if not self.charm.unit.is_leader(): + logging.debug("Not a leader unit, skipping setting endpoint") + return + + # If relation is not provided send endpoint to all the related + # applications. This happens usually when endpoint data is + # updated by provider and wants to send the data to all + # related applications + if relation is None: + logging.debug( + "Sending endpoint to all related applications of relation" + f"{self.relation_name}" + ) + for relation in self.framework.model.relations[self.relation_name]: + relation.data[self.charm.app]["endpoint"] = endpoint + else: + logging.debug( + f"Sending endpoint on relation {relation.app.name} " + f"{relation.name}/{relation.id}" + ) + relation.data[self.charm.app]["endpoint"] = endpoint + + +class DesignateEndpointChangedEvent(ops.RelationEvent): + """DesignateEndpointChanged Event.""" + + pass + + +class DesignateServiceGoneAwayEvent(ops.RelationEvent): + """DesignateServiceGoneAway Event.""" + + pass + + +class DesignateServiceRequirerEvents(ops.ObjectEvents): + """Events class for `on`.""" + + endpoint_changed = ops.EventSource(DesignateEndpointChangedEvent) + goneaway = ops.EventSource(DesignateServiceGoneAwayEvent) + + +class DesignateServiceRequires(ops.Object): + """Class to be instantiated by the requiring side of the relation.""" + + on = DesignateServiceRequirerEvents() + + def __init__(self, charm: ops.CharmBase, relation_name: str): + super().__init__(charm, relation_name) + self.charm = charm + self.relation_name = relation_name + self.framework.observe( + self.charm.on[relation_name].relation_changed, + self._on_relation_changed, + ) + self.framework.observe( + self.charm.on[relation_name].relation_broken, + self._on_relation_broken, + ) + + def _on_relation_changed(self, event: ops.RelationJoinedEvent): + """Handle relation changed event.""" + self.on.endpoint_changed.emit(event.relation) + + def _on_relation_broken(self, event: ops.RelationBrokenEvent): + """Handle relation broken event.""" + self.on.goneaway.emit(event.relation) + + @property + def _designate_service_rel(self) -> ops.Relation | None: + """The designate service relation.""" + return self.framework.model.get_relation(self.relation_name) + + def get_remote_app_data(self, key: str) -> str | None: + """Return the value for the given key from remote app data.""" + if self._designate_service_rel: + data = self._designate_service_rel.data[ + self._designate_service_rel.app + ] + return data.get(key) + + return None + + @property + def endpoint(self) -> str | None: + """Return the designate endpoint.""" + return self.get_remote_app_data("endpoint")