Fix support for network-spaces

Fix misc issues with use of Juju 2.0 network spaces:

 - Ensure that wsrep address for local unit is correctly
   set using the binding for the cluster peer relation.

 - Correctly set the DB access hostname for db and db-admin
   relation types.

This includes a little refactoring to support reuse within
the charm.

Closes-Bug: 1657305

Change-Id: Id1a800e2ada6fd196422b003fd8e251cab5ad724
This commit is contained in:
David Ames 2017-01-24 16:14:23 -08:00 committed by James Page
parent ae533965d6
commit 8815918715
4 changed files with 70 additions and 58 deletions

View File

@ -77,7 +77,7 @@ from charmhelpers.contrib.openstack.ha.utils import (
from percona_utils import ( from percona_utils import (
determine_packages, determine_packages,
setup_percona_repo, setup_percona_repo,
get_host_ip, resolve_hostname_to_ip,
get_cluster_hosts, get_cluster_hosts,
configure_sstuser, configure_sstuser,
configure_mysql_root_password, configure_mysql_root_password,
@ -96,6 +96,7 @@ from percona_utils import (
resolve_cnf_file, resolve_cnf_file,
create_binlogs_directory, create_binlogs_directory,
bootstrap_pxc, bootstrap_pxc,
get_cluster_host_ip,
) )
@ -138,7 +139,7 @@ def render_config(clustered=False, hosts=None):
context = { context = {
'cluster_name': 'juju_cluster', 'cluster_name': 'juju_cluster',
'private_address': get_host_ip(), 'private_address': get_cluster_host_ip(),
'clustered': clustered, 'clustered': clustered,
'cluster_hosts': ",".join(hosts), 'cluster_hosts': ",".join(hosts),
'sst_method': config('sst-method'), 'sst_method': config('sst-method'),
@ -328,17 +329,7 @@ def cluster_joined():
relation_settings = {'private-address': addr, relation_settings = {'private-address': addr,
'hostname': socket.gethostname()} 'hostname': socket.gethostname()}
cluster_network = config('cluster-network') relation_settings['cluster-address'] = get_cluster_host_ip()
if cluster_network:
cluster_addr = get_address_in_network(cluster_network, fatal=True)
relation_settings['cluster-address'] = cluster_addr
else:
try:
cluster_addr = network_get_primary_address('cluster')
relation_settings['cluster-address'] = cluster_addr
except NotImplementedError:
# NOTE(jamespage): skip - fallback to previous behaviour
pass
log("Setting cluster relation: '%s'" % (relation_settings), log("Setting cluster relation: '%s'" % (relation_settings),
level=INFO) level=INFO)
@ -383,22 +374,17 @@ def db_changed(relation_id=None, unit=None, admin=None):
relation_clear(relation_id) relation_clear(relation_id)
return return
if is_clustered():
db_host = config('vip')
else:
if config('prefer-ipv6'):
db_host = get_ipv6_addr(exc_list=[config('vip')])[0]
else:
db_host = unit_get('private-address')
if admin not in [True, False]: if admin not in [True, False]:
admin = relation_type() == 'db-admin' admin = relation_type() == 'db-admin'
db_name, _ = remote_unit().split("/") db_name, _ = remote_unit().split("/")
username = db_name username = db_name
db_helper = get_db_helper() db_helper = get_db_helper()
addr = relation_get('private-address', unit=unit, rid=relation_id) addr = relation_get('private-address', unit=unit, rid=relation_id)
password = db_helper.configure_db(addr, db_name, username, admin=admin) password = db_helper.configure_db(addr, db_name, username, admin=admin)
db_host = get_db_host(addr, interface=relation_type())
relation_set(relation_id=relation_id, relation_set(relation_id=relation_id,
relation_settings={ relation_settings={
'user': username, 'user': username,
@ -409,7 +395,7 @@ def db_changed(relation_id=None, unit=None, admin=None):
def get_db_host(client_hostname, interface='shared-db'): def get_db_host(client_hostname, interface='shared-db'):
"""Get address of local database host. """Get address of local database host for use by db clients
If an access-network has been configured, expect selected address to be If an access-network has been configured, expect selected address to be
on that network. If none can be found, revert to primary address. on that network. If none can be found, revert to primary address.
@ -417,16 +403,24 @@ def get_db_host(client_hostname, interface='shared-db'):
If network spaces are supported (Juju >= 2.0), use network-get to If network spaces are supported (Juju >= 2.0), use network-get to
retrieve the network binding for the interface. retrieve the network binding for the interface.
If DNSHA is set pass os-access-hostname
If vip(s) are configured, chooses first available. If vip(s) are configured, chooses first available.
@param client_hostname: hostname of client side relation setting hostname.
Only used if access-network is configured
@param interface: Network space binding to check.
Usually the relationship name.
@returns IP for use with db clients
""" """
vips = config('vip').split() if config('vip') else [] vips = config('vip').split() if config('vip') else []
dns_ha = config('dns-ha') dns_ha = config('dns-ha')
access_network = config('access-network') access_network = config('access-network')
client_ip = get_host_ip(client_hostname)
if is_clustered() and dns_ha: if is_clustered() and dns_ha:
log("Using DNS HA hostname: {}".format(config('os-access-hostname'))) log("Using DNS HA hostname: {}".format(config('os-access-hostname')))
return config('os-access-hostname') return config('os-access-hostname')
elif access_network: elif access_network:
client_ip = resolve_hostname_to_ip(client_hostname)
if is_address_in_network(access_network, client_ip): if is_address_in_network(access_network, client_ip):
if is_clustered(): if is_clustered():
for vip in vips: for vip in vips:
@ -463,6 +457,7 @@ def get_db_host(client_hostname, interface='shared-db'):
if config('prefer-ipv6'): if config('prefer-ipv6'):
return get_ipv6_addr(exc_list=vips)[0] return get_ipv6_addr(exc_list=vips)[0]
# Last resort
return unit_get('private-address') return unit_get('private-address')
@ -523,7 +518,7 @@ def shared_db_changed(relation_id=None, unit=None):
database = settings['database'] database = settings['database']
username = settings['username'] username = settings['username']
normalized_address = get_host_ip(hostname) normalized_address = resolve_hostname_to_ip(hostname)
if access_network and not is_address_in_network(access_network, if access_network and not is_address_in_network(access_network,
normalized_address): normalized_address):
# NOTE: for configurations using access-network, only setup # NOTE: for configurations using access-network, only setup
@ -583,7 +578,7 @@ def shared_db_changed(relation_id=None, unit=None):
hostname = databases[db]['hostname'] hostname = databases[db]['hostname']
username = databases[db]['username'] username = databases[db]['username']
normalized_address = get_host_ip(hostname) normalized_address = resolve_hostname_to_ip(hostname)
if (access_network and if (access_network and
not is_address_in_network(access_network, not is_address_in_network(access_network,
normalized_address)): normalized_address)):

View File

@ -102,7 +102,12 @@ def setup_percona_repo():
subprocess.check_call(['apt-key', 'add', KEY]) subprocess.check_call(['apt-key', 'add', KEY])
def get_host_ip(hostname=None): def resolve_hostname_to_ip(hostname):
"""Resolve hostname to IP
@param hostname: hostname to be resolved
@returns IP address
"""
try: try:
import dns.resolver import dns.resolver
except ImportError: except ImportError:
@ -110,12 +115,6 @@ def get_host_ip(hostname=None):
fatal=True) fatal=True)
import dns.resolver import dns.resolver
if config('prefer-ipv6'):
# Ensure we have a valid ipv6 address configured
get_ipv6_addr(exc_list=[config('vip')], fatal=True)[0]
return socket.gethostname()
hostname = hostname or unit_get('private-address')
try: try:
# Test to see if already an IPv4 address # Test to see if already an IPv4 address
socket.inet_aton(hostname) socket.inet_aton(hostname)
@ -154,23 +153,15 @@ def is_sufficient_peers():
def get_cluster_hosts(): def get_cluster_hosts():
hosts_map = {} hosts_map = {}
if config('cluster-network'): local_cluster_address = get_cluster_host_ip()
hostname = get_address_in_network(config('cluster-network'),
fatal=True)
else:
try:
hostname = network_get_primary_address('cluster')
except NotImplementedError:
# NOTE(jamespage): skip - fallback to previous behaviour
hostname = get_host_ip()
# We need to add this localhost dns name to /etc/hosts along with peer # We need to add this localhost dns name to /etc/hosts along with peer
# hosts to ensure percona gets consistently resolved addresses. # hosts to ensure percona gets consistently resolved addresses.
if config('prefer-ipv6'): if config('prefer-ipv6'):
addr = get_ipv6_addr(exc_list=[config('vip')], fatal=True)[0] addr = get_ipv6_addr(exc_list=[config('vip')], fatal=True)[0]
hosts_map = {addr: hostname} hosts_map = {addr: socket.gethostname()}
hosts = [hostname] hosts = [local_cluster_address]
for relid in relation_ids('cluster'): for relid in relation_ids('cluster'):
for unit in related_units(relid): for unit in related_units(relid):
rdata = relation_get(unit=unit, rid=relid) rdata = relation_get(unit=unit, rid=relid)
@ -192,7 +183,7 @@ def get_cluster_hosts():
hosts_map[cluster_address] = hostname hosts_map[cluster_address] = hostname
hosts.append(hostname) hosts.append(hostname)
else: else:
hosts.append(get_host_ip(cluster_address)) hosts.append(resolve_hostname_to_ip(cluster_address))
if hosts_map: if hosts_map:
update_hosts_file(hosts_map) update_hosts_file(hosts_map)
@ -570,3 +561,24 @@ def create_binlogs_directory():
if not os.path.isdir(binlogs_directory): if not os.path.isdir(binlogs_directory):
mkdir(binlogs_directory, 'mysql', 'mysql', 0o750) mkdir(binlogs_directory, 'mysql', 'mysql', 0o750)
def get_cluster_host_ip():
"""Get the this host's IP address for use with percona cluster peers
@returns IP to pass to cluster peers
"""
cluster_network = config('cluster-network')
if cluster_network:
cluster_addr = get_address_in_network(cluster_network, fatal=True)
else:
try:
cluster_addr = network_get_primary_address('cluster')
except NotImplementedError:
# NOTE(jamespage): fallback to previous behaviour
cluster_addr = resolve_hostname_to_ip(
unit_get('private-address')
)
return cluster_addr

View File

@ -26,7 +26,7 @@ TO_PATCH = ['log', 'config',
'network_get_primary_address', 'network_get_primary_address',
'resolve_network_cidr', 'resolve_network_cidr',
'unit_get', 'unit_get',
'get_host_ip', 'resolve_hostname_to_ip',
'is_clustered', 'is_clustered',
'get_ipv6_addr', 'get_ipv6_addr',
'get_hacluster_config', 'get_hacluster_config',
@ -165,7 +165,7 @@ class TestHostResolution(CharmTestCase):
Ensure that with nothing other than defaults private-address is used Ensure that with nothing other than defaults private-address is used
''' '''
self.unit_get.return_value = 'mydbhost' self.unit_get.return_value = 'mydbhost'
self.get_host_ip.return_value = '10.0.0.2' self.resolve_hostname_to_ip.return_value = '10.0.0.2'
self.assertEqual(hooks.get_db_host('myclient'), 'mydbhost') self.assertEqual(hooks.get_db_host('myclient'), 'mydbhost')
def test_get_db_host_network_spaces(self): def test_get_db_host_network_spaces(self):
@ -173,7 +173,7 @@ class TestHostResolution(CharmTestCase):
Ensure that if the shared-db relation is bound, its bound address Ensure that if the shared-db relation is bound, its bound address
is used is used
''' '''
self.get_host_ip.return_value = '10.0.0.2' self.resolve_hostname_to_ip.return_value = '10.0.0.2'
self.network_get_primary_address.side_effect = None self.network_get_primary_address.side_effect = None
self.network_get_primary_address.return_value = '192.168.20.2' self.network_get_primary_address.return_value = '192.168.20.2'
self.assertEqual(hooks.get_db_host('myclient'), '192.168.20.2') self.assertEqual(hooks.get_db_host('myclient'), '192.168.20.2')
@ -184,7 +184,7 @@ class TestHostResolution(CharmTestCase):
Ensure that if the shared-db relation is bound and the unit is Ensure that if the shared-db relation is bound and the unit is
clustered, that the correct VIP is chosen clustered, that the correct VIP is chosen
''' '''
self.get_host_ip.return_value = '10.0.0.2' self.resolve_hostname_to_ip.return_value = '10.0.0.2'
self.is_clustered.return_value = True self.is_clustered.return_value = True
self.test_config.set('vip', '10.0.0.100 192.168.20.200') self.test_config.set('vip', '10.0.0.100 192.168.20.200')
self.network_get_primary_address.side_effect = None self.network_get_primary_address.side_effect = None

View File

@ -73,23 +73,24 @@ class UtilsTests(unittest.TestCase):
self.assertEqual(lines[1], "%s %s\n" % (map.items()[0])) self.assertEqual(lines[1], "%s %s\n" % (map.items()[0]))
self.assertEqual(lines[4], "%s %s\n" % (map.items()[3])) self.assertEqual(lines[4], "%s %s\n" % (map.items()[3]))
@mock.patch("percona_utils.get_cluster_host_ip")
@mock.patch("percona_utils.log") @mock.patch("percona_utils.log")
@mock.patch("percona_utils.config") @mock.patch("percona_utils.config")
@mock.patch("percona_utils.update_hosts_file") @mock.patch("percona_utils.update_hosts_file")
@mock.patch("percona_utils.get_host_ip")
@mock.patch("percona_utils.relation_get") @mock.patch("percona_utils.relation_get")
@mock.patch("percona_utils.related_units") @mock.patch("percona_utils.related_units")
@mock.patch("percona_utils.relation_ids") @mock.patch("percona_utils.relation_ids")
def test_get_cluster_hosts(self, mock_rel_ids, mock_rel_units, def test_get_cluster_hosts(self, mock_rel_ids, mock_rel_units,
mock_rel_get, mock_get_host_ip, mock_rel_get,
mock_update_hosts_file, mock_config, mock_update_hosts_file, mock_config,
mock_log): mock_log,
mock_get_cluster_host_ip):
mock_rel_ids.return_value = [1] mock_rel_ids.return_value = [1]
mock_rel_units.return_value = [2] mock_rel_units.return_value = [2]
mock_get_host_ip.return_value = 'hostA' mock_get_cluster_host_ip.return_value = '10.2.0.1'
def _mock_rel_get(*args, **kwargs): def _mock_rel_get(*args, **kwargs):
return {'private-address': '0.0.0.0'} return {'private-address': '10.2.0.2'}
mock_rel_get.side_effect = _mock_rel_get mock_rel_get.side_effect = _mock_rel_get
mock_config.side_effect = lambda k: False mock_config.side_effect = lambda k: False
@ -98,25 +99,29 @@ class UtilsTests(unittest.TestCase):
self.assertFalse(mock_update_hosts_file.called) self.assertFalse(mock_update_hosts_file.called)
mock_rel_get.assert_called_with(rid=1, unit=2) mock_rel_get.assert_called_with(rid=1, unit=2)
self.assertEqual(hosts, ['hostA', 'hostA']) self.assertEqual(hosts, ['10.2.0.1', '10.2.0.2'])
@mock.patch.object(percona_utils, 'socket')
@mock.patch("percona_utils.get_cluster_host_ip")
@mock.patch.object(percona_utils, 'get_ipv6_addr') @mock.patch.object(percona_utils, 'get_ipv6_addr')
@mock.patch.object(percona_utils, 'log') @mock.patch.object(percona_utils, 'log')
@mock.patch.object(percona_utils, 'config') @mock.patch.object(percona_utils, 'config')
@mock.patch.object(percona_utils, 'update_hosts_file') @mock.patch.object(percona_utils, 'update_hosts_file')
@mock.patch.object(percona_utils, 'get_host_ip')
@mock.patch.object(percona_utils, 'relation_get') @mock.patch.object(percona_utils, 'relation_get')
@mock.patch.object(percona_utils, 'related_units') @mock.patch.object(percona_utils, 'related_units')
@mock.patch.object(percona_utils, 'relation_ids') @mock.patch.object(percona_utils, 'relation_ids')
def test_get_cluster_hosts_ipv6(self, mock_rel_ids, mock_rel_units, def test_get_cluster_hosts_ipv6(self, mock_rel_ids, mock_rel_units,
mock_rel_get, mock_get_host_ip, mock_rel_get,
mock_update_hosts_file, mock_config, mock_update_hosts_file, mock_config,
mock_log, mock_get_ipv6_addr): mock_log, mock_get_ipv6_addr,
mock_get_cluster_host_ip,
mock_socket):
ipv6addr = '2001:db8:1:0:f816:3eff:fe79:cd' ipv6addr = '2001:db8:1:0:f816:3eff:fe79:cd'
mock_get_ipv6_addr.return_value = [ipv6addr] mock_get_ipv6_addr.return_value = [ipv6addr]
mock_rel_ids.return_value = [88] mock_rel_ids.return_value = [88]
mock_rel_units.return_value = [1, 2] mock_rel_units.return_value = [1, 2]
mock_get_host_ip.return_value = 'hostA' mock_get_cluster_host_ip.return_value = 'hostA'
mock_socket.gethostname.return_value = 'hostA'
def _mock_rel_get(*args, **kwargs): def _mock_rel_get(*args, **kwargs):
host_suffix = 'BC' host_suffix = 'BC'