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
This commit is contained in:
Hemanth Nakkina 2024-05-06 13:38:51 +05:30
parent 708b9b6331
commit 77067e7b4c
No known key found for this signature in database
GPG Key ID: 2E4970F7B143168E
8 changed files with 444 additions and 0 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -60,6 +60,9 @@ requires:
receive-ca-cert:
interface: certificate_transfer
optional: true
external-dns:
interface: designate
optional: true
peers:
peers:

View File

@ -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 [

View File

@ -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 -%}

View File

@ -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=(

View File

@ -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")