Add functionality to configure virtual service IPs
Closes-Bug: #1804057 func-test-pr: https://github.com/openstack-charmers/zaza-openstack-tests/pull/685 Change-Id: I29f2eeca508048fdf8464193368cce5720559b9e
This commit is contained in:
parent
bb172c5c03
commit
4000b6ea72
@ -46,3 +46,15 @@ options:
|
|||||||
in a situation that upstream DNS servers do not support DNSSEC,
|
in a situation that upstream DNS servers do not support DNSSEC,
|
||||||
and BIND9 reports "Unable to fetch DNSKEY". For production
|
and BIND9 reports "Unable to fetch DNSKEY". For production
|
||||||
deployments, it's encouraged to keep DNSSEC enabled.
|
deployments, it's encouraged to keep DNSSEC enabled.
|
||||||
|
service_ips:
|
||||||
|
default: ""
|
||||||
|
type: string
|
||||||
|
description: |
|
||||||
|
Service IPs are list of Virtual IPs that will be assigned to the
|
||||||
|
designate-bind units. This option accepts comma separated list of
|
||||||
|
IPv4 or IPv6 addresses that belong to the at least one directly
|
||||||
|
connected network.
|
||||||
|
This option also requires that relation with subordinate hacluster
|
||||||
|
unit is created, otherwise the designate-bind units go into the the
|
||||||
|
blocked state until the hacluster relation is made, or this option
|
||||||
|
is uncofigured.
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
includes: ['layer:openstack', 'interface:bind-rndc', 'interface:openstack-ha']
|
includes:
|
||||||
|
- 'layer:openstack'
|
||||||
|
- 'interface:bind-rndc'
|
||||||
|
- 'interface:openstack-ha'
|
||||||
|
- 'interface:hacluster'
|
||||||
options:
|
options:
|
||||||
basic:
|
basic:
|
||||||
use_venv: True
|
use_venv: True
|
||||||
|
@ -20,6 +20,9 @@ import subprocess
|
|||||||
import hmac
|
import hmac
|
||||||
import hashlib
|
import hashlib
|
||||||
import base64
|
import base64
|
||||||
|
import json
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address, ip_address
|
||||||
|
|
||||||
import charms_openstack.charm as openstack_charm
|
import charms_openstack.charm as openstack_charm
|
||||||
import charms_openstack.adapters as adapters
|
import charms_openstack.adapters as adapters
|
||||||
@ -28,7 +31,9 @@ import charmhelpers.core.decorators as ch_decorators
|
|||||||
import charmhelpers.core.hookenv as hookenv
|
import charmhelpers.core.hookenv as hookenv
|
||||||
import charmhelpers.core.host as host
|
import charmhelpers.core.host as host
|
||||||
|
|
||||||
|
from charmhelpers.core.hookenv import WORKLOAD_STATES
|
||||||
from charmhelpers.contrib.network import ip as ch_ip
|
from charmhelpers.contrib.network import ip as ch_ip
|
||||||
|
from relations.hacluster.interface_hacluster.common import CRM
|
||||||
|
|
||||||
|
|
||||||
LEADERDB_SECRET_KEY = 'rndc_key'
|
LEADERDB_SECRET_KEY = 'rndc_key'
|
||||||
@ -37,6 +42,9 @@ LEADERDB_SYNC_TIME_KEY = 'sync_time'
|
|||||||
CLUSTER_SYNC_KEY = 'sync_request'
|
CLUSTER_SYNC_KEY = 'sync_request'
|
||||||
WWW_DIR = '/var/www/html'
|
WWW_DIR = '/var/www/html'
|
||||||
ZONE_DIR = '/var/cache/bind'
|
ZONE_DIR = '/var/cache/bind'
|
||||||
|
SERVICE_IP_PREFIX = "service_ip_"
|
||||||
|
COLOCATION_NAME = "service_ip_colocation"
|
||||||
|
AWAITING_HACLUSTER_FLAG = "service_ip.waiting_for_hacluster"
|
||||||
|
|
||||||
|
|
||||||
def install():
|
def install():
|
||||||
@ -151,6 +159,107 @@ def assess_status():
|
|||||||
DesignateBindCharm.singleton.assess_status()
|
DesignateBindCharm.singleton.assess_status()
|
||||||
|
|
||||||
|
|
||||||
|
def parse_service_ip_config():
|
||||||
|
"""Parse "service_ips" config option value into IP address objects.
|
||||||
|
|
||||||
|
Expected format is comma separated IPv4 or IPv6 addresses.
|
||||||
|
|
||||||
|
:raises: ValueError if "service_ips" is not in expected format.
|
||||||
|
:return: List of IPv4 or IPv6 addresses
|
||||||
|
:rtype: list[IPv4Address | ipaddress.IPv6Address]
|
||||||
|
"""
|
||||||
|
raw_service_ips = hookenv.config('service_ips')
|
||||||
|
service_ips = []
|
||||||
|
for ip_addr in raw_service_ips.split(","):
|
||||||
|
if ip_addr:
|
||||||
|
service_ips.append(ip_address(ip_addr.strip()))
|
||||||
|
return service_ips
|
||||||
|
|
||||||
|
|
||||||
|
def remove_service_ips(exclude=None):
|
||||||
|
"""Remove all service IPs except those stated in the `exclude` parameter.
|
||||||
|
|
||||||
|
:param exclude: List of IPs that should be kept alive.
|
||||||
|
:type exclude: list[IPv4Address | ipaddress.IPv6Address]
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
if exclude is None:
|
||||||
|
exclude = []
|
||||||
|
hookenv.log('Clearing Service IPs.', hookenv.INFO)
|
||||||
|
ha_cluster = reactive.endpoint_from_flag('ha.connected')
|
||||||
|
|
||||||
|
if not ha_cluster:
|
||||||
|
hookenv.log('No relation with "ha-cluster" charm. Nothing to clear.',
|
||||||
|
hookenv.WARNING)
|
||||||
|
return
|
||||||
|
|
||||||
|
managed_resources = json.loads(ha_cluster.get_local("json_resources") or
|
||||||
|
"{}")
|
||||||
|
excluded_ips = [str(addr) for addr in exclude]
|
||||||
|
for resource in managed_resources:
|
||||||
|
if resource.startswith(SERVICE_IP_PREFIX):
|
||||||
|
value = resource[len(SERVICE_IP_PREFIX):]
|
||||||
|
if value not in excluded_ips:
|
||||||
|
ha_cluster.delete_resource(resource)
|
||||||
|
|
||||||
|
ha_cluster.remove_colocation(COLOCATION_NAME)
|
||||||
|
ha_cluster.bind_resources()
|
||||||
|
|
||||||
|
|
||||||
|
def add_service_ips(service_ips):
|
||||||
|
"""Configure (virtual) service IPs on the unit.
|
||||||
|
|
||||||
|
These IPs and their assignment are handled by corosync (hacluster charm)
|
||||||
|
and by default they will spread to every unit of designate-bind. Id there
|
||||||
|
are more IPs configured than there are units, some units will ned up with
|
||||||
|
more than one IP.
|
||||||
|
|
||||||
|
:param service_ips: List of Service IPs to configure
|
||||||
|
:type service_ips: list[IPv4Address | ipaddress.IPv6Address]
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
ha_cluster = reactive.endpoint_from_flag('ha.connected')
|
||||||
|
|
||||||
|
if not service_ips:
|
||||||
|
hookenv.log('Skipping Service IP configuration. No IP addresses '
|
||||||
|
'provided', hookenv.DEBUG)
|
||||||
|
reactive.clear_flag(AWAITING_HACLUSTER_FLAG)
|
||||||
|
return
|
||||||
|
if not ha_cluster:
|
||||||
|
hookenv.log('Failed to configure service IPs. "ha-cluster" relation '
|
||||||
|
'missing', hookenv.ERROR)
|
||||||
|
reactive.set_flag(AWAITING_HACLUSTER_FLAG)
|
||||||
|
return
|
||||||
|
|
||||||
|
crm = CRM()
|
||||||
|
all_ip_resources = []
|
||||||
|
|
||||||
|
for ip in service_ips:
|
||||||
|
resource_name = "{}{}".format(SERVICE_IP_PREFIX, ip)
|
||||||
|
addr_type = "IPaddr2" if isinstance(ip, IPv4Address) else "IPv6addr"
|
||||||
|
all_ip_resources.append(resource_name)
|
||||||
|
crm.primitive(resource_name, 'ocf:heartbeat:{}'.format(addr_type),
|
||||||
|
params='ip={}'.format(ip), op='monitor interval="10s"')
|
||||||
|
|
||||||
|
crm.colocation(COLOCATION_NAME, -10, *all_ip_resources)
|
||||||
|
ha_cluster.manage_resources(crm)
|
||||||
|
|
||||||
|
|
||||||
|
def reconfigure_service_ips():
|
||||||
|
"""Configure IPs on units according to 'service_ips' config option."""
|
||||||
|
hookenv.log("Configuring Service IPs: {}".format(
|
||||||
|
hookenv.config('service_ips')), hookenv.INFO)
|
||||||
|
try:
|
||||||
|
service_ips = parse_service_ip_config()
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
remove_service_ips(exclude=service_ips)
|
||||||
|
add_service_ips(service_ips)
|
||||||
|
finally:
|
||||||
|
assess_status()
|
||||||
|
|
||||||
|
|
||||||
@adapters.adapter_property("dns-backend")
|
@adapters.adapter_property("dns-backend")
|
||||||
def dns_egress_subnets(dns_backend):
|
def dns_egress_subnets(dns_backend):
|
||||||
"""Generate list of CIDR's for remote units
|
"""Generate list of CIDR's for remote units
|
||||||
@ -508,3 +617,32 @@ class DesignateBindCharm(openstack_charm.OpenStackCharm):
|
|||||||
if not os.path.isfile(apparmor_file):
|
if not os.path.isfile(apparmor_file):
|
||||||
open(apparmor_file, 'w').close()
|
open(apparmor_file, 'w').close()
|
||||||
host.service_reload('apparmor')
|
host.service_reload('apparmor')
|
||||||
|
|
||||||
|
def _assess_status(self):
|
||||||
|
"""Overall unit status assessment.
|
||||||
|
|
||||||
|
This override appends additional status checks related to the
|
||||||
|
service_ip configuration.
|
||||||
|
"""
|
||||||
|
super()._assess_status()
|
||||||
|
# Subordinate charm "ha-cluster" is needed for management of Service
|
||||||
|
# IPs. If service IPs are configured but there's no relation with
|
||||||
|
# "hacluster", put unit into blocked state.
|
||||||
|
try:
|
||||||
|
parse_service_ip_config()
|
||||||
|
except ValueError:
|
||||||
|
bad_format_msg = ('Config option "service_ips" does not have '
|
||||||
|
'an expected format.')
|
||||||
|
hookenv.status_set(WORKLOAD_STATES.BLOCKED, bad_format_msg)
|
||||||
|
return
|
||||||
|
|
||||||
|
if reactive.is_flag_set(AWAITING_HACLUSTER_FLAG):
|
||||||
|
hookenv.log('Config option "service_ips" is set but relation with '
|
||||||
|
'"hacluster" is missing. Either add subordinate '
|
||||||
|
'"hacluster" charm or unconfigure "service_ips" '
|
||||||
|
'option',
|
||||||
|
hookenv.ERROR)
|
||||||
|
hookenv.status_set(WORKLOAD_STATES.BLOCKED, 'Failed to configure '
|
||||||
|
'"service_ips", '
|
||||||
|
'hacluster relation is'
|
||||||
|
' missing.')
|
||||||
|
@ -23,3 +23,6 @@ provides:
|
|||||||
peers:
|
peers:
|
||||||
cluster:
|
cluster:
|
||||||
interface: openstack-ha
|
interface: openstack-ha
|
||||||
|
requires:
|
||||||
|
ha:
|
||||||
|
interface: hacluster
|
||||||
|
@ -112,3 +112,27 @@ def process_sync_requests(hacluster):
|
|||||||
@reactive.when('zones.initialised')
|
@reactive.when('zones.initialised')
|
||||||
def assess_status():
|
def assess_status():
|
||||||
designate_bind.assess_status()
|
designate_bind.assess_status()
|
||||||
|
|
||||||
|
|
||||||
|
@reactive.when('config.changed.service_ips')
|
||||||
|
def service_ips_changed():
|
||||||
|
"""Reconfigure service IPs on the unit."""
|
||||||
|
designate_bind.reconfigure_service_ips()
|
||||||
|
|
||||||
|
|
||||||
|
@reactive.when('ha.connected')
|
||||||
|
def hacluster_connected(_):
|
||||||
|
"""Check if service IPs are awaiting configuration via hacluster."""
|
||||||
|
if reactive.is_flag_set(designate_bind.AWAITING_HACLUSTER_FLAG):
|
||||||
|
hookenv.log('hacluster connected, configuring Service IPs',
|
||||||
|
hookenv.INFO)
|
||||||
|
designate_bind.reconfigure_service_ips()
|
||||||
|
reactive.clear_flag(designate_bind.AWAITING_HACLUSTER_FLAG)
|
||||||
|
|
||||||
|
|
||||||
|
@reactive.when('ha-relation-departed')
|
||||||
|
def hacluster_departed(_):
|
||||||
|
"""Set blocked state if hacluster leaves and service_ips are configured."""
|
||||||
|
if hookenv.config('service_ips'):
|
||||||
|
reactive.set_flag(designate_bind.AWAITING_HACLUSTER_FLAG)
|
||||||
|
designate_bind.assess_status()
|
||||||
|
@ -101,6 +101,11 @@ applications:
|
|||||||
nrpe:
|
nrpe:
|
||||||
charm: cs:nrpe
|
charm: cs:nrpe
|
||||||
|
|
||||||
|
designate-bind-hacluster:
|
||||||
|
charm: cs:hacluster
|
||||||
|
options:
|
||||||
|
cluster_count: 1
|
||||||
|
|
||||||
relations:
|
relations:
|
||||||
- - 'keystone:shared-db'
|
- - 'keystone:shared-db'
|
||||||
- 'keystone-mysql-router:shared-db'
|
- 'keystone-mysql-router:shared-db'
|
||||||
@ -140,3 +145,6 @@ relations:
|
|||||||
|
|
||||||
- - 'designate:nrpe-external-master'
|
- - 'designate:nrpe-external-master'
|
||||||
- 'nrpe:nrpe-external-master'
|
- 'nrpe:nrpe-external-master'
|
||||||
|
|
||||||
|
- - 'designate-bind'
|
||||||
|
- 'designate-bind-hacluster'
|
||||||
|
@ -101,6 +101,11 @@ applications:
|
|||||||
nrpe:
|
nrpe:
|
||||||
charm: cs:nrpe
|
charm: cs:nrpe
|
||||||
|
|
||||||
|
designate-bind-hacluster:
|
||||||
|
charm: cs:hacluster
|
||||||
|
options:
|
||||||
|
cluster_count: 1
|
||||||
|
|
||||||
relations:
|
relations:
|
||||||
- - 'keystone:shared-db'
|
- - 'keystone:shared-db'
|
||||||
- 'keystone-mysql-router:shared-db'
|
- 'keystone-mysql-router:shared-db'
|
||||||
@ -140,3 +145,6 @@ relations:
|
|||||||
|
|
||||||
- - 'designate:nrpe-external-master'
|
- - 'designate:nrpe-external-master'
|
||||||
- 'nrpe:nrpe-external-master'
|
- 'nrpe:nrpe-external-master'
|
||||||
|
|
||||||
|
- - 'designate-bind'
|
||||||
|
- 'designate-bind-hacluster'
|
||||||
|
@ -102,6 +102,11 @@ applications:
|
|||||||
charm: cs:nrpe
|
charm: cs:nrpe
|
||||||
series: focal
|
series: focal
|
||||||
|
|
||||||
|
designate-bind-hacluster:
|
||||||
|
charm: cs:hacluster
|
||||||
|
options:
|
||||||
|
cluster_count: 1
|
||||||
|
|
||||||
relations:
|
relations:
|
||||||
- - 'keystone:shared-db'
|
- - 'keystone:shared-db'
|
||||||
- 'keystone-mysql-router:shared-db'
|
- 'keystone-mysql-router:shared-db'
|
||||||
@ -141,3 +146,6 @@ relations:
|
|||||||
|
|
||||||
- - 'designate:nrpe-external-master'
|
- - 'designate:nrpe-external-master'
|
||||||
- 'nrpe:nrpe-external-master'
|
- 'nrpe:nrpe-external-master'
|
||||||
|
|
||||||
|
- - 'designate-bind'
|
||||||
|
- 'designate-bind-hacluster'
|
||||||
|
@ -102,6 +102,11 @@ applications:
|
|||||||
charm: cs:nrpe
|
charm: cs:nrpe
|
||||||
series: focal
|
series: focal
|
||||||
|
|
||||||
|
designate-bind-hacluster:
|
||||||
|
charm: cs:hacluster
|
||||||
|
options:
|
||||||
|
cluster_count: 1
|
||||||
|
|
||||||
relations:
|
relations:
|
||||||
- - 'keystone:shared-db'
|
- - 'keystone:shared-db'
|
||||||
- 'keystone-mysql-router:shared-db'
|
- 'keystone-mysql-router:shared-db'
|
||||||
@ -141,3 +146,6 @@ relations:
|
|||||||
|
|
||||||
- - 'designate:nrpe-external-master'
|
- - 'designate:nrpe-external-master'
|
||||||
- 'nrpe:nrpe-external-master'
|
- 'nrpe:nrpe-external-master'
|
||||||
|
|
||||||
|
- - 'designate-bind'
|
||||||
|
- 'designate-bind-hacluster'
|
||||||
|
@ -13,6 +13,7 @@ dev_bundles:
|
|||||||
|
|
||||||
tests:
|
tests:
|
||||||
- zaza.openstack.charm_tests.designate.tests.DesignateTests
|
- zaza.openstack.charm_tests.designate.tests.DesignateTests
|
||||||
|
- zaza.openstack.charm_tests.designate_bind.tests.DesignateBindServiceIPsTest
|
||||||
|
|
||||||
target_deploy_status:
|
target_deploy_status:
|
||||||
nrpe:
|
nrpe:
|
||||||
|
@ -15,6 +15,12 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Modules imported from other interfaces/layers need to be mocked
|
||||||
|
sys.modules[
|
||||||
|
'relations.hacluster.interface_hacluster.common'
|
||||||
|
] = mock.MagicMock()
|
||||||
|
|
||||||
import reactive.designate_bind_handlers as handlers
|
import reactive.designate_bind_handlers as handlers
|
||||||
|
|
||||||
@ -120,6 +126,9 @@ class TestDesignateHandlers(unittest.TestCase):
|
|||||||
('zones.initialised', ),
|
('zones.initialised', ),
|
||||||
],
|
],
|
||||||
'assess_status': [('zones.initialised', )],
|
'assess_status': [('zones.initialised', )],
|
||||||
|
'service_ips_changed': [('config.changed.service_ips', )],
|
||||||
|
'hacluster_connected': [('ha.connected', )],
|
||||||
|
'hacluster_departed': [('ha-relation-departed',)],
|
||||||
}
|
}
|
||||||
when_not_patterns = {
|
when_not_patterns = {
|
||||||
'install_packages': [('installed', )],
|
'install_packages': [('installed', )],
|
||||||
@ -236,3 +245,42 @@ class TestDesignateHandlers(unittest.TestCase):
|
|||||||
self.is_leader.return_value = True
|
self.is_leader.return_value = True
|
||||||
handlers.process_sync_requests('hacluster')
|
handlers.process_sync_requests('hacluster')
|
||||||
self.process_requests.assert_called_once_with('hacluster')
|
self.process_requests.assert_called_once_with('hacluster')
|
||||||
|
|
||||||
|
def test_configure_service_ips(self):
|
||||||
|
"""Test that change of 'service_ips' triggers reconfiguration."""
|
||||||
|
self.patch(handlers.designate_bind, 'reconfigure_service_ips')
|
||||||
|
|
||||||
|
handlers.service_ips_changed()
|
||||||
|
|
||||||
|
handlers.designate_bind.reconfigure_service_ips.assert_called_once()
|
||||||
|
|
||||||
|
def test_hacluster_connected_reconfigure_service_ips(self):
|
||||||
|
"""Test that hacluster connect hook configures 'service_ips' on join.
|
||||||
|
|
||||||
|
When relation with hacluster is joined and 'service_ips' are waiting
|
||||||
|
for configuration, 'reconfigure_service_ips' should be trigerred.
|
||||||
|
"""
|
||||||
|
self.patch(handlers.designate_bind, 'reconfigure_service_ips')
|
||||||
|
self.patch(handlers.reactive, 'is_flag_set', True)
|
||||||
|
self.patch(handlers.reactive, 'clear_flag')
|
||||||
|
|
||||||
|
handlers.hacluster_connected(None)
|
||||||
|
|
||||||
|
handlers.designate_bind.reconfigure_service_ips.assert_called_once()
|
||||||
|
handlers.reactive.clear_flag.assert_called_once_with(
|
||||||
|
handlers.designate_bind.AWAITING_HACLUSTER_FLAG
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_hacluster_connect_no_service_ips(self):
|
||||||
|
"""Test that hacluster connect hook passes if there are no IPs.
|
||||||
|
|
||||||
|
If there are no service_ips configured when the hacluster joins,
|
||||||
|
this hook should not execute anything.
|
||||||
|
"""
|
||||||
|
self.patch(handlers.designate_bind, 'reconfigure_service_ips')
|
||||||
|
self.patch(handlers.reactive, 'is_flag_set', False)
|
||||||
|
self.patch(handlers.reactive, 'clear_flag')
|
||||||
|
|
||||||
|
handlers.hacluster_connected(None)
|
||||||
|
|
||||||
|
handlers.designate_bind.reconfigure_service_ips.assert_not_called()
|
||||||
|
@ -14,6 +14,14 @@
|
|||||||
|
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from ipaddress import IPv4Address, IPv6Address
|
||||||
|
|
||||||
|
# Modules imported from other interfaces/layers need to be mocked
|
||||||
|
sys.modules[
|
||||||
|
'relations.hacluster.interface_hacluster.common'
|
||||||
|
] = mock.MagicMock()
|
||||||
|
|
||||||
import charms_openstack.test_utils as test_utils
|
import charms_openstack.test_utils as test_utils
|
||||||
|
|
||||||
@ -141,6 +149,123 @@ class TestOpenStackDesignateBind(Helper):
|
|||||||
self.render_with_interfaces.assert_called_once_with('interface_list')
|
self.render_with_interfaces.assert_called_once_with('interface_list')
|
||||||
|
|
||||||
|
|
||||||
|
class TestServiceIPFunctions(Helper):
|
||||||
|
"""Collection of tests for functions doing 'service_ip' configuration."""
|
||||||
|
|
||||||
|
def test_parse_service_ip_config(self):
|
||||||
|
"""Test function parsing 'service_ips' config option."""
|
||||||
|
service_ips = "10.0.0.1, 10.0.0.2, 2001:db8::2:1"
|
||||||
|
|
||||||
|
with mock.patch.object(designate_bind.hookenv, 'config') as config:
|
||||||
|
config.return_value = service_ips
|
||||||
|
parsed_ips = designate_bind.parse_service_ip_config()
|
||||||
|
|
||||||
|
expected_output = [
|
||||||
|
IPv4Address("10.0.0.1"),
|
||||||
|
IPv4Address("10.0.0.2"),
|
||||||
|
IPv6Address("2001:db8::2:1")
|
||||||
|
]
|
||||||
|
|
||||||
|
self.assertEqual(expected_output, parsed_ips)
|
||||||
|
|
||||||
|
def test_remove_service_ips(self):
|
||||||
|
"""Test removing already configured Service IPs."""
|
||||||
|
ip_to_remove = IPv4Address("10.0.0.1")
|
||||||
|
ip_to_retain = IPv4Address("10.0.0.2")
|
||||||
|
configured_resources = '["{0}{1}", "{0}{2}"]'.format(
|
||||||
|
designate_bind.SERVICE_IP_PREFIX,
|
||||||
|
ip_to_remove,
|
||||||
|
ip_to_retain
|
||||||
|
)
|
||||||
|
ha_cluster = mock.MagicMock()
|
||||||
|
ha_cluster.get_local.return_value = configured_resources
|
||||||
|
|
||||||
|
with mock.patch.object(designate_bind.reactive, 'endpoint_from_flag',
|
||||||
|
return_value=ha_cluster):
|
||||||
|
designate_bind.remove_service_ips(exclude=[ip_to_retain])
|
||||||
|
|
||||||
|
ha_cluster.delete_resource.assert_called_once_with("{}{}".format(
|
||||||
|
designate_bind.SERVICE_IP_PREFIX, ip_to_remove
|
||||||
|
))
|
||||||
|
ha_cluster.remove_colocation.assert_called_once_with(
|
||||||
|
designate_bind.COLOCATION_NAME
|
||||||
|
)
|
||||||
|
ha_cluster.bind_resources.assert_called_once_with()
|
||||||
|
|
||||||
|
def test_remove_service_ips_no_hacluster(self):
|
||||||
|
"""Test that warning is logged if there's no hacluster."""
|
||||||
|
with mock.patch.object(designate_bind.reactive, 'endpoint_from_flag',
|
||||||
|
return_value=None):
|
||||||
|
designate_bind.remove_service_ips()
|
||||||
|
|
||||||
|
expected_message = ('No relation with "ha-cluster" charm. Nothing to '
|
||||||
|
'clear.')
|
||||||
|
expected_level = designate_bind.hookenv.WARNING
|
||||||
|
designate_bind.hookenv.log.assert_called_with(expected_message,
|
||||||
|
expected_level)
|
||||||
|
|
||||||
|
def test_add_service_ips(self):
|
||||||
|
"""Test configuring Service IP via hacluster resources."""
|
||||||
|
ha_cluster_mock = mock.MagicMock()
|
||||||
|
crm_mock = mock.MagicMock()
|
||||||
|
crm_patch = mock.patch.object(designate_bind, 'CRM',
|
||||||
|
return_value=crm_mock)
|
||||||
|
crm_patch.start()
|
||||||
|
|
||||||
|
add_ips = [IPv4Address("10.0.0.1"), IPv6Address("2001:db8::2:1")]
|
||||||
|
expected_primitive_calls = []
|
||||||
|
all_ip_resources = []
|
||||||
|
for ip_ in add_ips:
|
||||||
|
resource = "{}{}".format(designate_bind.SERVICE_IP_PREFIX, ip_)
|
||||||
|
ip_type = "IPaddr2" if isinstance(ip_, IPv4Address) else "IPv6addr"
|
||||||
|
expected_primitive_calls.append(mock.call(
|
||||||
|
resource,
|
||||||
|
"ocf:heartbeat:{}".format(ip_type),
|
||||||
|
params="ip={}".format(ip_),
|
||||||
|
op='monitor interval="10s"'
|
||||||
|
))
|
||||||
|
all_ip_resources.append(resource)
|
||||||
|
|
||||||
|
with mock.patch.object(designate_bind.reactive, 'endpoint_from_flag',
|
||||||
|
return_value=ha_cluster_mock):
|
||||||
|
designate_bind.add_service_ips(add_ips)
|
||||||
|
|
||||||
|
crm_mock.primitive.assert_has_calls(expected_primitive_calls)
|
||||||
|
crm_mock.colocation.assert_called_once_with(
|
||||||
|
designate_bind.COLOCATION_NAME,
|
||||||
|
-10,
|
||||||
|
*all_ip_resources
|
||||||
|
)
|
||||||
|
ha_cluster_mock.manage_resources.assert_called_once_with(crm_mock)
|
||||||
|
|
||||||
|
crm_patch.stop()
|
||||||
|
|
||||||
|
def test_add_service_ips_no_config(self):
|
||||||
|
"""Test that function passes when there are no IPs to configure."""
|
||||||
|
ha_cluster_mock = mock.MagicMock()
|
||||||
|
|
||||||
|
with mock.patch.object(designate_bind.reactive, 'endpoint_from_flag',
|
||||||
|
return_value=ha_cluster_mock):
|
||||||
|
designate_bind.add_service_ips([])
|
||||||
|
|
||||||
|
ha_cluster_mock.manage_resources.assert_not_called()
|
||||||
|
# Assert that we also clear flag that signals that service_ips config
|
||||||
|
# option is configured and charm is waiting for hacluster relation.
|
||||||
|
designate_bind.reactive.clear_flag.assert_called_with(
|
||||||
|
designate_bind.AWAITING_HACLUSTER_FLAG
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_add_service_ips_no_hacluster(self):
|
||||||
|
"""Test that function sets correct flag if it's missing hacluster."""
|
||||||
|
with mock.patch.object(designate_bind.reactive, 'endpoint_from_flag',
|
||||||
|
return_value=None):
|
||||||
|
designate_bind.add_service_ips([IPv4Address("10.0.0.1")])
|
||||||
|
|
||||||
|
designate_bind.reactive.set_flag.assert_called_with(
|
||||||
|
designate_bind.AWAITING_HACLUSTER_FLAG
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class TestEgressSubnets(Helper):
|
class TestEgressSubnets(Helper):
|
||||||
|
|
||||||
def test_egress_subnets(self):
|
def test_egress_subnets(self):
|
||||||
@ -481,3 +606,56 @@ class TestDesignateBindCharm(Helper):
|
|||||||
bob.assert_called_once_with(
|
bob.assert_called_once_with(
|
||||||
'/etc/apparmor.d/disable/usr.sbin.named',
|
'/etc/apparmor.d/disable/usr.sbin.named',
|
||||||
'w')
|
'w')
|
||||||
|
|
||||||
|
def test_asses_status_passes(self):
|
||||||
|
"""Test scenario where _assess_status passes without blocking charm."""
|
||||||
|
self.patch_object(designate_bind.openstack_charm.OpenStackCharm,
|
||||||
|
'_assess_status')
|
||||||
|
self.patch_object(designate_bind, 'parse_service_ip_config')
|
||||||
|
designate_bind.reactive.is_flag_set.return_value = False
|
||||||
|
|
||||||
|
charm_ = designate_bind.DesignateBindCharm()
|
||||||
|
charm_._assess_status()
|
||||||
|
|
||||||
|
designate_bind.parse_service_ip_config.assert_called_once_with()
|
||||||
|
designate_bind.hookenv.status_set.assert_not_called()
|
||||||
|
|
||||||
|
def test_assess_status_bad_ip_config(self):
|
||||||
|
"""Test that unit is blocked if 'service_ips' config has bad format."""
|
||||||
|
self.patch_object(designate_bind.openstack_charm.OpenStackCharm,
|
||||||
|
'_assess_status')
|
||||||
|
self.patch_object(designate_bind, 'parse_service_ip_config')
|
||||||
|
designate_bind.parse_service_ip_config.side_effect = ValueError
|
||||||
|
|
||||||
|
charm_ = designate_bind.DesignateBindCharm()
|
||||||
|
charm_._assess_status()
|
||||||
|
|
||||||
|
expected_state = designate_bind.hookenv.WORKLOAD_STATES.BLOCKED
|
||||||
|
expected_message = ('Config option "service_ips" does not have an '
|
||||||
|
'expected format.')
|
||||||
|
|
||||||
|
designate_bind.hookenv.status_set.assert_called_once_with(
|
||||||
|
expected_state, expected_message
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_assess_status_blocks_on_awaiting_hacluster(self):
|
||||||
|
"""Test that unit is blocked if it's awaiting hacluster relation.
|
||||||
|
|
||||||
|
This occurs when 'service_ips' config option is set but relation with
|
||||||
|
hacluster charm is missing.
|
||||||
|
"""
|
||||||
|
self.patch_object(designate_bind.openstack_charm.OpenStackCharm,
|
||||||
|
'_assess_status')
|
||||||
|
self.patch_object(designate_bind, 'parse_service_ip_config')
|
||||||
|
designate_bind.reactive.is_flag_set.return_value = True
|
||||||
|
|
||||||
|
charm_ = designate_bind.DesignateBindCharm()
|
||||||
|
charm_._assess_status()
|
||||||
|
|
||||||
|
expected_state = designate_bind.hookenv.WORKLOAD_STATES.BLOCKED
|
||||||
|
expected_message = ('Failed to configure "service_ips", hacluster '
|
||||||
|
'relation is missing.')
|
||||||
|
|
||||||
|
designate_bind.hookenv.status_set.assert_called_once_with(
|
||||||
|
expected_state, expected_message
|
||||||
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user