Add support for loadbalancer interface

This adds support for the new loadbalancer interface which is intended
to allow for load balancer / ingress endpoint providers, such as the
cloud integrator charms, to provide a load balancer address upon
request. The initial use-case for this is using Vault in Azure, where it
is difficult or impossible to use a VIP or floating IP type approach for
HA Vault; instead, this will allow a relation to the azure-integrator
charm which will provide a native Azure LB which Vault can then
advertise.

Change-Id: I5e0738429d47625c23bfe71c86df6266a3ea364b
This commit is contained in:
Cory Johns 2021-02-23 17:51:19 -05:00
parent 8923bc9f86
commit 7f4c95b5b4
5 changed files with 122 additions and 3 deletions

View File

@ -49,7 +49,8 @@ options:
Virtual IP to use api traffic. You can provide up to two addresses
configured on the access or external bindings. If neither binding
is used then you can only provide one address that must be configured
on the default space.
on the default space. Mutually exclusive with the dns-ha-access-record
config option or lb-provider relation.
channel:
type: string
default: stable
@ -59,8 +60,8 @@ options:
type: string
default:
description: |
DNS record to use for DNS HA with MAAS. Do not use vip setting
if this is set.
DNS record to use for DNS HA with MAAS. Mutually exclusive with the
vip config option or lb-provider relation.
totally-unsecure-auto-unlock:
type: boolean
default: false

View File

@ -32,6 +32,11 @@ requires:
ha:
interface: hacluster
scope: container
lb-provider:
# Optional relation to a load balancer provider. Mutually exclusive
# with the vip or dns-ha-access-record config options.
interface: loadbalancer
limit: 1
provides:
nrpe-external-master:
interface: nrpe-external-master

View File

@ -66,6 +66,7 @@ from charms.reactive import (
from charms.reactive.relations import (
endpoint_from_flag,
endpoint_from_name,
)
from charms.reactive.flags import (
@ -283,6 +284,7 @@ def upgrade_charm():
remove_state('configured')
remove_state('vault.nrpe.configured')
remove_state('vault.ssl.configured')
remove_state('vault.requested-lb')
@when_not("is-update-status-hook")
@ -560,10 +562,29 @@ def configure_secrets_backend():
clear_flag('secrets.refresh')
@when('endpoint.lb-provider.available')
@when('leadership.is_leader')
@when_not('vault.requested-lb')
def request_lb():
lb_provider = endpoint_from_name('lb-provider')
req = lb_provider.get_request('vault')
req.protocol = req.protocols.tcp
req.port_mapping = {8220: 8220}
lb_provider.send_request(req)
set_flag('vault.requested-lb')
@when('vault.requested-lb')
@when_not('endpoint.lb-provider.available')
def clear_requested_lb():
clear_flag('vault.requested-lb')
@when_not("is-update-status-hook")
@when('secrets.connected')
def send_vault_url_and_ca():
secrets = endpoint_from_flag('secrets.connected')
lb_provider = endpoint_from_name('lb-provider')
vault_url_external = None
hostname = config('hostname')
vip = vault.get_vip()
@ -580,6 +601,16 @@ def send_vault_url_and_ca():
log("VIP is set but ha.available is not yet set, skipping "
"send_vault_url_and_ca.", level=DEBUG)
return
elif lb_provider.has_response:
response = lb_provider.get_response('vault')
if response.error:
log('Load balancer failed, skipping: '
'{}'.format(response.error_message or response.error_fields),
level=ERROR)
return
vault_url = vault.get_api_url(address=response.address)
vault_url_external = vault_url
lb_provider.ack_response(response)
else:
vault_url = vault.get_api_url()
vault_url_external = vault.get_api_url(binding='external')
@ -694,6 +725,15 @@ def _assess_status():
status_set('blocked',
'vip and dns-ha-access-record configured')
return
if is_flag_set('config.lb_vip.invalid'):
status_set('blocked',
'lb-provider and vip are mutually exclusive')
return
if is_flag_set('config.lb_dns.invalid'):
status_set('blocked',
'lb-provider and dns-ha-access-record are '
'mutually exclusive')
return
if unitdata.kv().get('charm.vault.series-upgrading'):
status_set("blocked",
@ -756,6 +796,20 @@ def _assess_status():
status_set('blocked', 'Vault cannot authorize approle')
return
lb_provider = endpoint_from_name('lb-provider')
is_leader = is_flag_set('leadership.is_leader')
if is_leader and lb_provider.is_available:
if not lb_provider.has_response:
status_set('waiting', 'Waiting for load balancer')
return
response = lb_provider.get_response('vault')
if response.error:
status_set('blocked',
'Load balancer failed: '
'{}'.format(response.error_message or
response.error_fields))
return
has_ca = is_flag_set('charm.vault.ca.ready')
has_cert_reqs = is_flag_set('certificates.certs.requested')
if has_cert_reqs and not has_ca:

View File

@ -10,3 +10,5 @@ psutil
git+https://opendev.org/openstack/charms.openstack.git#egg=charms.openstack
git+https://github.com/juju/charm-helpers.git#egg=charmhelpers
loadbalancer-interface

View File

@ -52,6 +52,7 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
self.patches = [
'config',
'endpoint_from_flag',
'endpoint_from_name',
'is_state',
'log',
'network_get_primary_address',
@ -78,6 +79,9 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
self.kv = mock.MagicMock()
self.kv.get.return_value = False
self.unitdata.kv.return_value = self.kv
self.endpoint_from_name().is_available = False
self.endpoint_from_name().has_response = False
self.patch_object(handlers.vault.hookenv, 'charm_dir', 'src')
def test_ssl_available(self):
self.assertFalse(handlers.ssl_available({
@ -1095,3 +1099,56 @@ class TestHandlers(unit_tests.test_utils.CharmTestCase):
assert service_running.call_count == 2
set_flag.assert_called_once_with('started')
prepare_vault.assert_called_once_with()
def test_loadbalancer(self):
self.is_flag_set.return_value = False
self.patch_object(handlers.vault, 'get_vip', return_value=None)
mock_secrets = self.endpoint_from_flag()
lb_provider = self.endpoint_from_name()
lb_provider.has_response = True
response = lb_provider.get_response()
response.success = False
handlers.send_vault_url_and_ca()
self.assertFalse(mock_secrets.publish_url.called)
response.error = None
response.address = 'loadbalancer'
handlers.send_vault_url_and_ca()
lb_provider.ack_response.assert_called_with(response)
mock_secrets.publish_url.assert_has_calls([
call(vault_url='http://loadbalancer:8200',
remote_binding='access'),
call(vault_url='http://loadbalancer:8200',
remote_binding='external'),
])
@patch.object(handlers, 'leader_get')
@patch.object(handlers, 'client_approle_authorized')
@patch.object(handlers, '_assess_interface_groups')
@patch.object(handlers.vault, 'get_vault_health')
def test_assess_status_loadbalancer(self, get_vault_health,
_assess_interface_groups,
_client_approle_authorized,
_leader_get):
self.is_flag_set.return_value = False
get_vault_health.return_value = self._health_response
self.endpoint_from_name().is_available = True
self.endpoint_from_name().has_response = False
handlers._assess_status()
self.status_set.assert_called_with(
'active', mock.ANY
)
self.is_flag_set.side_effect = lambda f: f == 'leadership.is_leader'
handlers._assess_status()
self.status_set.assert_called_with(
'waiting', 'Waiting for load balancer'
)
self.endpoint_from_name().has_response = True
self.endpoint_from_name().get_response().error = True
self.endpoint_from_name().get_response().error_message = 'just because'
handlers._assess_status()
self.status_set.assert_called_with(
'blocked', 'Load balancer failed: just because'
)