Configure live migration

Configure TLS certificates with the right extensions to be used in an
mTLS environment. (Used by Libvirt and QEMU for native TLS migration).
Ask for new TLS certificate if it's missing the mTLS clientAuth and
serverAuth extended key usages.
Libvirt/QEMU fail to read CA certificate with chain, therefore it's
templated without the chain.
Add extra binding `migration`.
Add extra configuration key `use-migration-binding`. It's false by
default, since on current sunbeam installation, there's no space
configuration, all ip addresses are part of the alpha space. Which makes
selecting the right ip address impossible.

Change-Id: Ia0622b12bcac6b90d7a9937695947c113f62d7fe
This commit is contained in:
Guillaume Boutry 2023-11-17 09:52:34 +01:00
parent df70e376ff
commit 66da01ee71
No known key found for this signature in database
GPG Key ID: E95E3326872E55DE
4 changed files with 181 additions and 7 deletions

View File

@ -23,3 +23,6 @@ options:
physnet-name:
default: "physnet1"
type: string
use-migration-binding:
default: False
type: boolean

View File

@ -28,6 +28,9 @@ provides:
cos-agent:
interface: cos_agent
extra-bindings:
migration:
# This charm has no peer relation by design. This charm needs to scale to
# hundreds of units and this is limited by the peer relation.

View File

@ -45,6 +45,9 @@ from charms.ceilometer_k8s.v0.ceilometer_service import (
from charms.grafana_agent.v0.cos_agent import (
COSAgentProvider,
)
from cryptography import (
x509,
)
from ops.charm import (
ActionEvent,
)
@ -57,6 +60,119 @@ from utils import (
logger = logging.getLogger(__name__)
MIGRATION_BINDING = "migration"
MTLS_USAGES = {x509.OID_SERVER_AUTH, x509.OID_CLIENT_AUTH}
class MTlsCertificatesHandler(sunbeam_rhandlers.TlsCertificatesHandler):
"""Handler for certificates interface."""
def update_relation_data(self):
"""Update relation outside of relation context."""
relations = self.model.relations[self.relation_name]
if len(relations) != 1:
logger.debug(
f"Unit has wrong number of {self.relation_name!r} relations."
)
return
relation = relations[0]
csr = self._get_csr_from_relation_unit_data()
if not csr:
self._request_certificates()
return
certs = self._get_cert_from_relation_data(csr)
if "cert" not in certs or not self._has_certificate_mtls_extensions(
certs["cert"]
):
logger.info(
"Requesting new certificates, current is missing mTLS extensions."
)
relation.data[self.model.unit][
"certificate_signing_requests"
] = "[]"
self._request_certificates()
def _has_certificate_mtls_extensions(self, certificate: str) -> bool:
"""Check current certificate has mTLS extensions."""
cert = x509.load_pem_x509_certificate(certificate.encode())
for extension in cert.extensions:
if extension.oid != x509.OID_EXTENDED_KEY_USAGE:
continue
extension_oids = {ext.dotted_string for ext in extension.value}
mtls_oids = {oid.dotted_string for oid in MTLS_USAGES}
if mtls_oids.issubset(extension_oids):
return True
return False
def _request_certificates(self):
"""Request certificates from remote provider."""
# Lazy import to ensure this lib is only required if the charm
# has this relation.
from charms.tls_certificates_interface.v1.tls_certificates import (
generate_csr,
)
if self.ready:
logger.debug("Certificate request already complete.")
return
if self.private_key:
logger.debug("Private key found, requesting certificates")
else:
logger.debug("Cannot request certificates, private key not found")
return
csr = generate_csr(
private_key=self.private_key.encode(),
subject=socket.getfqdn(),
sans_dns=self.sans_dns,
sans_ip=self.sans_ips,
additional_critical_extensions=[
x509.KeyUsage(
digital_signature=True,
content_commitment=False,
key_encipherment=True,
data_encipherment=False,
key_agreement=True,
key_cert_sign=False,
crl_sign=False,
encipher_only=False,
decipher_only=False,
),
x509.ExtendedKeyUsage(MTLS_USAGES),
],
)
self.certificates.request_certificate_creation(
certificate_signing_request=csr
)
def context(self) -> dict:
"""Certificates context."""
csr_from_unit = self._get_csr_from_relation_unit_data()
if not csr_from_unit:
return {}
certs = self._get_cert_from_relation_data(csr_from_unit)
cert = certs["cert"]
ca_cert = certs["ca"]
ca_with_intermediates = certs["ca"] + "\n" + "\n".join(certs["chain"])
ctxt = {
"key": self.private_key,
"cert": cert,
"ca_cert": ca_cert,
"ca_with_intermediates": ca_with_intermediates,
}
return ctxt
@property
def ready(self) -> bool:
"""Whether handler ready for use."""
try:
return super().ready
except KeyError:
return False
class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
"""Charm the service."""
@ -98,6 +214,20 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
],
)
@property
def migration_address(self) -> Optional[str]:
"""Get address from migration binding."""
use_binding = self.model.config.get("use-migration-binding")
if not use_binding:
return None
binding = self.model.get_binding(MIGRATION_BINDING)
if binding is None:
return None
address = binding.network.bind_address
if address is None:
return None
return str(address)
def check_relation_exists(self, relation_name: str) -> bool:
"""Check if a relation exists or not."""
if self.model.get_relation(relation_name):
@ -135,6 +265,16 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
)
)
handlers.append(self.ceilometer)
if self.can_add_handler("certificates", handlers):
self.certs = MTlsCertificatesHandler(
self,
"certificates",
self.configure_charm,
sans_dns=self.get_sans_dns(),
sans_ips=self.get_sans_ips(),
mandatory="certificates" in self.mandatory_relations,
)
handlers.append(self.certs)
handlers = super().get_relation_handlers(handlers)
return handlers
@ -245,6 +385,18 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
"compute.spice-proxy-address": config("ip-address")
or local_ip,
"compute.virt-type": "kvm",
"compute.cacert": base64.b64encode(
contexts.certificates.ca_cert.encode()
).decode(),
"compute.cert": base64.b64encode(
contexts.certificates.cert.encode()
).decode(),
"compute.key": base64.b64encode(
contexts.certificates.key.encode()
).decode(),
"compute.migration-address": self.migration_address
or config("ip-address")
or local_ip,
"credentials.ovn-metadata-proxy-shared-secret": self.metadata_secret(),
"identity.admin-role": contexts.identity_credentials.admin_role,
"identity.auth-url": contexts.identity_credentials.internal_endpoint,
@ -273,7 +425,7 @@ class HypervisorOperatorCharm(sunbeam_charm.OSBaseOperatorCharm):
contexts.certificates.cert.encode()
).decode(),
"network.ovn-cacert": base64.b64encode(
contexts.certificates.ca_cert.encode()
contexts.certificates.ca_with_intermediates.encode()
).decode(),
"network.ovn-sb-connection": sb_connection_strs[0],
"network.physnet-name": config("physnet-name"),

View File

@ -113,10 +113,14 @@ class TestCharm(test_utils.CharmTestCase):
"latest", channel="essex/stable"
)
metadata = self.harness.charm.metadata_secret()
ovn_cacert = (
cacert = test_utils.TEST_CA
cacert_with_intermediates = (
test_utils.TEST_CA + "\n" + "\n".join(test_utils.TEST_CHAIN)
)
ovn_cacert = base64.b64encode(ovn_cacert.encode()).decode()
cacert = base64.b64encode(cacert.encode()).decode()
cacert_with_intermediates = base64.b64encode(
cacert_with_intermediates.encode()
).decode()
private_key = base64.b64encode(
self.harness.charm.contexts().certificates.key.encode()
).decode()
@ -127,6 +131,10 @@ class TestCharm(test_utils.CharmTestCase):
"compute.cpu-mode": "host-model",
"compute.spice-proxy-address": "10.0.0.10",
"compute.virt-type": "kvm",
"compute.cacert": cacert,
"compute.cert": certificate,
"compute.key": private_key,
"compute.migration-address": "10.0.0.10",
"compute.rbd-user": "nova",
"compute.rbd-secret-uuid": "ddd",
"compute.rbd-key": "eee",
@ -149,7 +157,7 @@ class TestCharm(test_utils.CharmTestCase):
"network.external-bridge": "br-ex",
"network.external-bridge-address": "10.20.20.1/24",
"network.ip-address": "10.0.0.10",
"network.ovn-cacert": ovn_cacert,
"network.ovn-cacert": cacert_with_intermediates,
"network.ovn-cert": certificate,
"network.ovn-key": private_key,
"network.ovn-sb-connection": "ssl:10.20.21.10:6642",
@ -198,10 +206,14 @@ class TestCharm(test_utils.CharmTestCase):
"latest", channel="essex/stable"
)
metadata = self.harness.charm.metadata_secret()
ovn_cacert = (
cacert = test_utils.TEST_CA
cacert_with_intermediates = (
test_utils.TEST_CA + "\n" + "\n".join(test_utils.TEST_CHAIN)
)
ovn_cacert = base64.b64encode(ovn_cacert.encode()).decode()
cacert = base64.b64encode(cacert.encode()).decode()
cacert_with_intermediates = base64.b64encode(
cacert_with_intermediates.encode()
).decode()
private_key = base64.b64encode(
self.harness.charm.contexts().certificates.key.encode()
).decode()
@ -212,6 +224,10 @@ class TestCharm(test_utils.CharmTestCase):
"compute.cpu-mode": "host-model",
"compute.spice-proxy-address": "10.0.0.10",
"compute.virt-type": "kvm",
"compute.cacert": cacert,
"compute.cert": certificate,
"compute.key": private_key,
"compute.migration-address": "10.0.0.10",
"compute.rbd-user": "nova",
"compute.rbd-secret-uuid": "ddd",
"compute.rbd-key": "eee",
@ -234,7 +250,7 @@ class TestCharm(test_utils.CharmTestCase):
"network.external-bridge": "br-ex",
"network.external-bridge-address": "10.20.20.1/24",
"network.ip-address": "10.0.0.10",
"network.ovn-cacert": ovn_cacert,
"network.ovn-cacert": cacert_with_intermediates,
"network.ovn-cert": certificate,
"network.ovn-key": private_key,
"network.ovn-sb-connection": "ssl:10.20.21.10:6642",