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:
parent
df70e376ff
commit
66da01ee71
@ -23,3 +23,6 @@ options:
|
||||
physnet-name:
|
||||
default: "physnet1"
|
||||
type: string
|
||||
use-migration-binding:
|
||||
default: False
|
||||
type: boolean
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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"),
|
||||
|
@ -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",
|
||||
|
Loading…
x
Reference in New Issue
Block a user