charm-hacluster/unit_tests/test_hacluster_utils.py
Liam Young e02c6257ae Fix adding of stonith controlled resources.
There appears to be a window between a pacemaker remote resource
being added and the location properties for that resource being
added. In this window the resource is down and pacemaker may fence
the node.

The window is present because the charm charm currently does:

1) Set stonith-enabled=true cluster property
2) Add maas stonith device that controls pacemaker remote node that
   has not yet been added.
3) Add pacemaker remote node
4) Add pacemaker location rules.

I think the following two fixes are needed:

1) For initial deploys update the charm so it does not enable stonith
   until stonith resources and pacemaker remotes have been added.

2) For scale-out do not add the new pacemaker remote stonith resource
   until the corresponding pacemaker resource has been added along
   with its location rules.

Depends-On: Ib8a667d0d82ef3dcd4da27e62460b4f0ce32ee43
Change-Id: I7e2f568d829f6d0bfc7859a7d0ea239203bbc490
Closes-Bug: #1884284
2020-09-09 09:35:30 +00:00

1261 lines
51 KiB
Python

# Copyright 2016 Canonical Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import contextlib
import json
import mock
import os
import re
import shutil
import subprocess
import tempfile
import unittest
import utils
import pcmk
def write_file(path, content, *args, **kwargs):
with open(path, 'wt') as f:
f.write(content)
f.flush()
@mock.patch.object(utils, 'log', lambda *args, **kwargs: None)
@mock.patch.object(utils, 'write_file', write_file)
class UtilsTestCaseWriteTmp(unittest.TestCase):
def setUp(self):
self.tmpdir = tempfile.mkdtemp()
utils.COROSYNC_CONF = os.path.join(self.tmpdir, 'corosync.conf')
def tearDown(self):
shutil.rmtree(self.tmpdir)
@mock.patch.object(utils, 'get_ha_nodes', lambda *args: {'1': '10.0.0.1'})
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
@mock.patch.object(utils, 'relation_ids')
@mock.patch.object(utils, 'get_network_address')
@mock.patch.object(utils, 'config')
def check_debug(self, enabled, mock_config, get_network_address,
relation_ids, related_units, relation_get):
cfg = {'debug': enabled,
'prefer-ipv6': False,
'corosync_mcastport': '1234',
'corosync_transport': 'udpu',
'corosync_mcastaddr': 'corosync_mcastaddr'}
def c(k):
return cfg.get(k)
mock_config.side_effect = c
get_network_address.return_value = "127.0.0.1"
relation_ids.return_value = ['foo:1']
related_units.return_value = ['unit-machine-0']
relation_get.return_value = 'iface'
conf = utils.get_corosync_conf()
if enabled:
self.assertEqual(conf['debug'], enabled)
else:
self.assertFalse('debug' in conf)
self.assertTrue(utils.emit_corosync_conf())
with open(utils.COROSYNC_CONF, 'rt') as fd:
content = fd.read()
if enabled:
pattern = 'debug: on\n'
else:
pattern = 'debug: off\n'
matches = re.findall(pattern, content, re.M)
self.assertEqual(len(matches), 2, str(matches))
def test_debug_on(self):
self.check_debug(True)
def test_debug_off(self):
self.check_debug(False)
class UtilsTestCase(unittest.TestCase):
@mock.patch.object(utils, 'config')
def test_get_transport(self, mock_config):
mock_config.return_value = 'udp'
self.assertEqual('udp', utils.get_transport())
mock_config.return_value = 'udpu'
self.assertEqual('udpu', utils.get_transport())
mock_config.return_value = 'hafu'
self.assertRaises(ValueError, utils.get_transport)
def test_nulls(self):
self.assertEqual(utils.nulls({'a': '', 'b': None, 'c': False}),
['a', 'b'])
@mock.patch.object(utils, 'local_unit', lambda *args: 'hanode/0')
@mock.patch.object(utils, 'get_ipv6_addr')
@mock.patch.object(utils, 'get_host_ip')
@mock.patch.object(utils.utils, 'is_ipv6', lambda *args: None)
@mock.patch.object(utils, 'get_corosync_id', lambda u: "%s-cid" % (u))
@mock.patch.object(utils, 'peer_ips', lambda *args, **kwargs:
{'hanode/1': '10.0.0.2'})
@mock.patch.object(utils, 'unit_get')
@mock.patch.object(utils, 'config')
def test_get_ha_nodes(self, mock_config, mock_unit_get, mock_get_host_ip,
mock_get_ipv6_addr):
mock_get_host_ip.side_effect = lambda host: host
def unit_get(key):
return {'private-address': '10.0.0.1'}.get(key)
mock_unit_get.side_effect = unit_get
def config(key):
return {'prefer-ipv6': False}.get(key)
mock_config.side_effect = config
nodes = utils.get_ha_nodes()
self.assertEqual(nodes, {'hanode/0-cid': '10.0.0.1',
'hanode/1-cid': '10.0.0.2'})
self.assertTrue(mock_get_host_ip.called)
self.assertFalse(mock_get_ipv6_addr.called)
@mock.patch.object(utils, 'local_unit', lambda *args: 'hanode/0')
@mock.patch.object(utils, 'get_ipv6_addr')
@mock.patch.object(utils, 'get_host_ip')
@mock.patch.object(utils.utils, 'is_ipv6')
@mock.patch.object(utils, 'get_corosync_id', lambda u: "%s-cid" % (u))
@mock.patch.object(utils, 'peer_ips', lambda *args, **kwargs:
{'hanode/1': '2001:db8:1::2'})
@mock.patch.object(utils, 'unit_get')
@mock.patch.object(utils, 'config')
def test_get_ha_nodes_ipv6(self, mock_config, mock_unit_get, mock_is_ipv6,
mock_get_host_ip, mock_get_ipv6_addr):
mock_get_ipv6_addr.return_value = '2001:db8:1::1'
mock_get_host_ip.side_effect = lambda host: host
def unit_get(key):
return {'private-address': '10.0.0.1'}.get(key)
mock_unit_get.side_effect = unit_get
def config(key):
return {'prefer-ipv6': True}.get(key)
mock_config.side_effect = config
nodes = utils.get_ha_nodes()
self.assertEqual(nodes, {'hanode/0-cid': '2001:db8:1::1',
'hanode/1-cid': '2001:db8:1::2'})
self.assertFalse(mock_get_host_ip.called)
self.assertTrue(mock_get_ipv6_addr.called)
@mock.patch.object(utils, 'assert_charm_supports_dns_ha')
@mock.patch.object(utils, 'config')
def test_validate_dns_ha_valid(self, config,
assert_charm_supports_dns_ha):
cfg = {'maas_url': 'http://maas/MAAAS/',
'maas_credentials': 'secret'}
config.side_effect = lambda key: cfg.get(key)
self.assertTrue(utils.validate_dns_ha())
self.assertTrue(assert_charm_supports_dns_ha.called)
@mock.patch.object(utils, 'assert_charm_supports_dns_ha')
@mock.patch.object(utils, 'status_set')
@mock.patch.object(utils, 'config')
def test_validate_dns_ha_invalid(self, config, status_set,
assert_charm_supports_dns_ha):
cfg = {'maas_url': 'http://maas/MAAAS/',
'maas_credentials': None}
config.side_effect = lambda key: cfg.get(key)
self.assertRaises(utils.MAASConfigIncomplete,
lambda: utils.validate_dns_ha())
self.assertTrue(assert_charm_supports_dns_ha.called)
status_set.assert_not_called()
@mock.patch.object(utils, 'apt_install')
@mock.patch.object(utils, 'apt_update')
@mock.patch.object(utils, 'add_source')
@mock.patch.object(utils, 'config')
def test_setup_maas_api(self, config, add_source, apt_update, apt_install):
cfg = {'maas_source': 'ppa:maas/stable',
'maas_source_key': None}
config.side_effect = lambda key: cfg.get(key)
utils.setup_maas_api()
add_source.assert_called_with(cfg['maas_source'],
cfg['maas_source_key'])
self.assertTrue(apt_install.called)
@mock.patch('os.path.isfile')
def test_ocf_file_exists(self, isfile_mock):
RES_NAME = 'res_ceilometer_agent_central'
resources = {RES_NAME: ('ocf:openstack:ceilometer-agent-central')}
utils.ocf_file_exists(RES_NAME, resources)
wish = '/usr/lib/ocf/resource.d/openstack/ceilometer-agent-central'
isfile_mock.assert_called_once_with(wish)
@mock.patch.object(subprocess, 'check_output')
@mock.patch.object(subprocess, 'call')
def test_kill_legacy_ocf_daemon_process(self, call_mock,
check_output_mock):
ps_output = b'''
PID CMD
6863 sshd: ubuntu@pts/7
11109 /usr/bin/python /usr/bin/ceilometer-agent-central --config
'''
check_output_mock.return_value = ps_output
utils.kill_legacy_ocf_daemon_process('res_ceilometer_agent_central')
call_mock.assert_called_once_with(['sudo', 'kill', '-9', '11109'])
@mock.patch.object(pcmk, 'wait_for_pcmk')
def test_try_pcmk_wait(self, mock_wait_for_pcmk):
# Returns OK
mock_wait_for_pcmk.side_effect = None
self.assertEqual(None, utils.try_pcmk_wait())
# Raises Exception
mock_wait_for_pcmk.side_effect = pcmk.ServicesNotUp
with self.assertRaises(pcmk.ServicesNotUp):
utils.try_pcmk_wait()
@mock.patch.object(pcmk, 'wait_for_pcmk')
@mock.patch.object(utils, 'service_running')
def test_services_running(self, mock_service_running,
mock_wait_for_pcmk):
# OS not running
mock_service_running.return_value = False
self.assertFalse(utils.services_running())
# Functional not running
mock_service_running.return_value = True
mock_wait_for_pcmk.side_effect = pcmk.ServicesNotUp
with self.assertRaises(pcmk.ServicesNotUp):
utils.services_running()
# All running
mock_service_running.return_value = True
mock_wait_for_pcmk.side_effect = None
mock_wait_for_pcmk.return_value = True
self.assertTrue(utils.services_running())
@mock.patch.object(pcmk, 'wait_for_pcmk')
@mock.patch.object(utils, 'restart_corosync')
def test_validated_restart_corosync(self, mock_restart_corosync,
mock_wait_for_pcmk):
# Services are down
mock_restart_corosync.mock_calls = []
mock_restart_corosync.return_value = False
with self.assertRaises(pcmk.ServicesNotUp):
utils.validated_restart_corosync(retries=3)
self.assertEqual(3, len(mock_restart_corosync.mock_calls))
# Services are up
mock_restart_corosync.mock_calls = []
mock_restart_corosync.return_value = True
utils.validated_restart_corosync(retries=10)
self.assertEqual(1, len(mock_restart_corosync.mock_calls))
@mock.patch.object(utils, 'is_unit_paused_set')
@mock.patch.object(utils, 'services_running')
@mock.patch.object(utils, 'service_start')
@mock.patch.object(utils, 'service_stop')
@mock.patch.object(utils, 'service_running')
def test_restart_corosync(self, mock_service_running,
mock_service_stop, mock_service_start,
mock_services_running, mock_is_unit_paused_set):
# PM up, services down
mock_service_running.return_value = True
mock_is_unit_paused_set.return_value = False
mock_services_running.return_value = False
self.assertFalse(utils.restart_corosync())
mock_service_stop.assert_has_calls([mock.call('pacemaker'),
mock.call('corosync')])
mock_service_start.assert_has_calls([mock.call('corosync'),
mock.call('pacemaker')])
# PM already down, services down
mock_service_running.return_value = False
mock_is_unit_paused_set.return_value = False
mock_services_running.return_value = False
self.assertFalse(utils.restart_corosync())
mock_service_stop.assert_has_calls([mock.call('corosync')])
mock_service_start.assert_has_calls([mock.call('corosync'),
mock.call('pacemaker')])
# PM already down, services up
mock_service_running.return_value = True
mock_is_unit_paused_set.return_value = False
mock_services_running.return_value = True
self.assertTrue(utils.restart_corosync())
mock_service_stop.assert_has_calls([mock.call('pacemaker'),
mock.call('corosync')])
mock_service_start.assert_has_calls([mock.call('corosync'),
mock.call('pacemaker')])
@mock.patch.object(subprocess, 'check_call')
@mock.patch.object(utils.os, 'mkdir')
@mock.patch.object(utils.os.path, 'exists')
@mock.patch.object(utils, 'render_template')
@mock.patch.object(utils, 'write_file')
@mock.patch.object(utils, 'is_unit_paused_set')
@mock.patch.object(utils, 'config')
def test_emit_systemd_overrides_file(self, mock_config,
mock_is_unit_paused_set,
mock_write_file, mock_render_template,
mock_path_exists,
mock_mkdir, mock_check_call):
# Normal values
cfg = {'service_stop_timeout': 30,
'service_start_timeout': 90}
mock_config.side_effect = lambda key: cfg.get(key)
mock_is_unit_paused_set.return_value = True
mock_path_exists.return_value = True
utils.emit_systemd_overrides_file()
self.assertEqual(2, len(mock_write_file.mock_calls))
mock_render_template.assert_has_calls(
[mock.call('systemd-overrides.conf', cfg),
mock.call('systemd-overrides.conf', cfg)])
mock_check_call.assert_has_calls([mock.call(['systemctl',
'daemon-reload'])])
mock_write_file.mock_calls = []
mock_render_template.mock_calls = []
mock_check_call.mock_calls = []
# Disable timeout
cfg = {'service_stop_timeout': -1,
'service_start_timeout': -1}
expected_cfg = {'service_stop_timeout': 'infinity',
'service_start_timeout': 'infinity'}
mock_config.side_effect = lambda key: cfg.get(key)
mock_is_unit_paused_set.return_value = True
mock_path_exists.return_value = True
utils.emit_systemd_overrides_file()
self.assertEqual(2, len(mock_write_file.mock_calls))
mock_render_template.assert_has_calls(
[mock.call('systemd-overrides.conf', expected_cfg),
mock.call('systemd-overrides.conf', expected_cfg)])
mock_check_call.assert_has_calls([mock.call(['systemctl',
'daemon-reload'])])
@mock.patch('pcmk.set_property')
@mock.patch('pcmk.get_property')
def test_maintenance_mode(self, mock_get_property, mock_set_property):
# enable maintenance-mode
mock_get_property.return_value = 'false\n'
utils.maintenance_mode(True)
mock_get_property.assert_called_with('maintenance-mode')
mock_set_property.assert_called_with('maintenance-mode', 'true')
mock_get_property.reset_mock()
mock_set_property.reset_mock()
mock_get_property.return_value = 'true\n'
utils.maintenance_mode(True)
mock_get_property.assert_called_with('maintenance-mode')
mock_set_property.assert_not_called()
# disable maintenance-mode
mock_get_property.return_value = 'true\n'
utils.maintenance_mode(False)
mock_get_property.assert_called_with('maintenance-mode')
mock_set_property.assert_called_with('maintenance-mode', 'false')
mock_get_property.reset_mock()
mock_set_property.reset_mock()
mock_get_property.return_value = 'false\n'
utils.maintenance_mode(False)
mock_get_property.assert_called_with('maintenance-mode')
mock_set_property.assert_not_called()
@mock.patch('subprocess.check_call')
def test_needs_maas_dns_migration(self, check_call):
ret = utils.needs_maas_dns_migration()
self.assertEqual(True, ret)
check_call.side_effect = subprocess.CalledProcessError(1, '')
ret = utils.needs_maas_dns_migration()
self.assertEqual(False, ret)
def test_get_ip_addr_from_resource_params(self):
param_str = 'params fqdn="keystone.maas" ip_address="{}" '
for addr in ("172.16.0.4", "2001:0db8:85a3:0000:0000:8a2e:0370:7334"):
ip = utils.get_ip_addr_from_resource_params(param_str.format(addr))
self.assertEqual(addr, ip)
ip = utils.get_ip_addr_from_resource_params("no_ip_addr")
self.assertEqual(None, ip)
@mock.patch.object(utils, 'write_file')
@mock.patch.object(utils, 'mkdir')
def test_write_maas_dns_address(self, mkdir, write_file):
utils.write_maas_dns_address("res_keystone_public_hostname",
"172.16.0.1")
mkdir.assert_called_once_with("/etc/maas_dns")
write_file.assert_called_once_with(
"/etc/maas_dns/res_keystone_public_hostname", content="172.16.0.1")
@mock.patch.object(utils, 'relation_get')
def test_parse_data_legacy(self, relation_get):
_rel_data = {
'testkey': repr({'test': 1})
}
relation_get.side_effect = lambda key, relid, unit: _rel_data.get(key)
self.assertEqual(utils.parse_data('hacluster:1',
'neutron-api/0',
'testkey'),
{'test': 1})
relation_get.assert_has_calls([
mock.call('json_testkey', 'neutron-api/0', 'hacluster:1'),
mock.call('testkey', 'neutron-api/0', 'hacluster:1'),
])
@mock.patch('pcmk.commit')
@mock.patch.object(utils, 'config')
@mock.patch.object(utils, 'configure_pacemaker_remote_stonith_resource')
def test_configure_stonith_stonith_enabled_false(
self,
mock_cfg_pcmkr_rstonith_res,
mock_config,
mock_commit):
cfg = {
'stonith_enabled': 'false'}
mock_config.side_effect = lambda key: cfg.get(key)
mock_cfg_pcmkr_rstonith_res.return_value = []
utils.configure_stonith()
mock_commit.assert_called_once_with(
'crm configure property stonith-enabled=false')
@mock.patch.object(utils, 'relation_get')
def test_parse_data_json(self, relation_get):
_rel_data = {
'json_testkey': json.dumps({'test': 1}),
'testkey': repr({'test': 1})
}
relation_get.side_effect = lambda key, relid, unit: _rel_data.get(key)
self.assertEqual(utils.parse_data('hacluster:1',
'neutron-api/0',
'testkey'),
{'test': 1})
# NOTE(jamespage): as json is the preferred format, the call for
# testkey should not occur.
relation_get.assert_has_calls([
mock.call('json_testkey', 'neutron-api/0', 'hacluster:1'),
])
@mock.patch.object(utils, 'render_template')
@mock.patch.object(utils.os.path, 'isdir')
@mock.patch.object(utils.os, 'mkdir')
@mock.patch.object(utils, 'write_file')
@mock.patch.object(utils, 'config')
def test_emit_base_conf(self, config, write_file, mkdir, isdir,
render_template):
cfg = {
'corosync_key': 'Y29yb3N5bmNrZXkK',
'pacemaker_key': 'cGFjZW1ha2Vya2V5Cg==',
}
config.side_effect = lambda x: cfg.get(x)
isdir.return_value = False
render = {
'corosync': 'corosync etc default config',
'hacluster.acl': 'hacluster acl file',
}
render_template.side_effect = lambda x, y: render[x]
expect_write_calls = [
mock.call(
content='corosync etc default config',
path='/etc/default/corosync'),
mock.call(
content='hacluster acl file',
path='/etc/corosync/uidgid.d/hacluster'),
mock.call(
content=b'corosynckey\n',
path='/etc/corosync/authkey',
perms=256),
mock.call(
content=b'pacemakerkey\n',
path='/etc/pacemaker/authkey',
perms=288,
group='haclient',
owner='root')
]
expect_render_calls = [
mock.call(
'corosync',
{'corosync_enabled': 'yes'}),
mock.call(
'hacluster.acl',
{})
]
mkdir_calls = [
mock.call('/etc/corosync/uidgid.d'),
mock.call('/etc/pacemaker'),
]
self.assertTrue(utils.emit_base_conf())
write_file.assert_has_calls(expect_write_calls)
render_template.assert_has_calls(expect_render_calls)
mkdir.assert_has_calls(mkdir_calls)
@mock.patch.object(utils, 'render_template')
@mock.patch.object(utils.os.path, 'isdir')
@mock.patch.object(utils.os, 'mkdir')
@mock.patch.object(utils, 'write_file')
@mock.patch.object(utils, 'config')
def test_emit_base_conf_no_pcmkr_key(self, config, write_file, mkdir,
isdir, render_template):
cfg = {
'corosync_key': 'Y29yb3N5bmNrZXkK',
}
config.side_effect = lambda x: cfg.get(x)
isdir.return_value = False
render = {
'corosync': 'corosync etc default config',
'hacluster.acl': 'hacluster acl file',
}
render_template.side_effect = lambda x, y: render[x]
expect_write_calls = [
mock.call(
content='corosync etc default config',
path='/etc/default/corosync'),
mock.call(
content='hacluster acl file',
path='/etc/corosync/uidgid.d/hacluster'),
mock.call(
content=b'corosynckey\n',
path='/etc/corosync/authkey',
perms=256),
mock.call(
content=b'corosynckey\n',
path='/etc/pacemaker/authkey',
perms=288,
group='haclient',
owner='root')
]
expect_render_calls = [
mock.call(
'corosync',
{'corosync_enabled': 'yes'}),
mock.call(
'hacluster.acl',
{})
]
mkdir_calls = [
mock.call('/etc/corosync/uidgid.d'),
mock.call('/etc/pacemaker'),
]
self.assertTrue(utils.emit_base_conf())
write_file.assert_has_calls(expect_write_calls)
render_template.assert_has_calls(expect_render_calls)
mkdir.assert_has_calls(mkdir_calls)
@mock.patch.object(utils, 'render_template')
@mock.patch.object(utils.os.path, 'isdir')
@mock.patch.object(utils.os, 'mkdir')
@mock.patch.object(utils, 'write_file')
@mock.patch.object(utils, 'config')
def test_emit_base_conf_no_coro_key(self, config, write_file, mkdir,
isdir, render_template):
cfg = {
}
config.side_effect = lambda x: cfg.get(x)
isdir.return_value = False
render = {
'corosync': 'corosync etc default config',
'hacluster.acl': 'hacluster acl file',
}
render_template.side_effect = lambda x, y: render[x]
expect_write_calls = [
mock.call(
content='corosync etc default config',
path='/etc/default/corosync'),
mock.call(
content='hacluster acl file',
path='/etc/corosync/uidgid.d/hacluster'),
]
expect_render_calls = [
mock.call(
'corosync',
{'corosync_enabled': 'yes'}),
mock.call(
'hacluster.acl',
{})
]
mkdir_calls = [
mock.call('/etc/corosync/uidgid.d'),
mock.call('/etc/pacemaker'),
]
self.assertFalse(utils.emit_base_conf())
write_file.assert_has_calls(expect_write_calls)
render_template.assert_has_calls(expect_render_calls)
mkdir.assert_has_calls(mkdir_calls)
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
@mock.patch.object(utils, 'relation_ids')
def test_need_resources_on_remotes_all_false(self, relation_ids,
related_units, relation_get):
rdata = {
'pacemaker-remote:49': {
'pacemaker-remote/0': {'enable-resources': "false"},
'pacemaker-remote/1': {'enable-resources': "false"},
'pacemaker-remote/2': {'enable-resources': "false"}}}
relation_ids.side_effect = lambda x: rdata.keys()
related_units.side_effect = lambda x: rdata[x].keys()
relation_get.side_effect = lambda x, y, z: rdata[z][y].get(x)
self.assertFalse(utils.need_resources_on_remotes())
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
@mock.patch.object(utils, 'relation_ids')
def test_need_resources_on_remotes_all_true(self, relation_ids,
related_units,
relation_get):
rdata = {
'pacemaker-remote:49': {
'pacemaker-remote/0': {'enable-resources': "true"},
'pacemaker-remote/1': {'enable-resources': "true"},
'pacemaker-remote/2': {'enable-resources': "true"}}}
relation_ids.side_effect = lambda x: rdata.keys()
related_units.side_effect = lambda x: rdata[x].keys()
relation_get.side_effect = lambda x, y, z: rdata[z][y].get(x)
self.assertTrue(utils.need_resources_on_remotes())
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
@mock.patch.object(utils, 'relation_ids')
def test_need_resources_on_remotes_mix(self, relation_ids, related_units,
relation_get):
rdata = {
'pacemaker-remote:49': {
'pacemaker-remote/0': {'enable-resources': "true"},
'pacemaker-remote/1': {'enable-resources': "false"},
'pacemaker-remote/2': {'enable-resources': "true"}}}
relation_ids.side_effect = lambda x: rdata.keys()
related_units.side_effect = lambda x: rdata[x].keys()
relation_get.side_effect = lambda x, y, z: rdata[z][y].get(x)
with self.assertRaises(ValueError):
self.assertTrue(utils.need_resources_on_remotes())
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
@mock.patch.object(utils, 'relation_ids')
def test_need_resources_on_remotes_missing(self, relation_ids,
related_units,
relation_get):
rdata = {
'pacemaker-remote:49': {
'pacemaker-remote/0': {},
'pacemaker-remote/1': {},
'pacemaker-remote/2': {}}}
relation_ids.side_effect = lambda x: rdata.keys()
related_units.side_effect = lambda x: rdata[x].keys()
relation_get.side_effect = lambda x, y, z: rdata[z][y].get(x, None)
with self.assertRaises(ValueError):
self.assertTrue(utils.need_resources_on_remotes())
@mock.patch.object(utils, 'need_resources_on_remotes')
@mock.patch('pcmk.commit')
def test_set_cluster_symmetry_true(self, commit,
need_resources_on_remotes):
need_resources_on_remotes.return_value = True
utils.set_cluster_symmetry()
commit.assert_called_once_with(
'crm configure property symmetric-cluster=true',
failure_is_fatal=True)
@mock.patch.object(utils, 'need_resources_on_remotes')
@mock.patch('pcmk.commit')
def test_set_cluster_symmetry_false(self, commit,
need_resources_on_remotes):
need_resources_on_remotes.return_value = False
utils.set_cluster_symmetry()
commit.assert_called_once_with(
'crm configure property symmetric-cluster=false',
failure_is_fatal=True)
@mock.patch.object(utils, 'need_resources_on_remotes')
@mock.patch('pcmk.commit')
def test_set_cluster_symmetry_unknown(self, commit,
need_resources_on_remotes):
need_resources_on_remotes.side_effect = ValueError()
utils.set_cluster_symmetry()
self.assertFalse(commit.called)
@mock.patch('pcmk.crm_update_location')
def test_add_score_location_rule(self, crm_update_location):
# Check no update required
utils.add_score_location_rule('res1', 'juju-lxd-0', 0)
crm_update_location.assert_called_once_with(
'loc-res1-juju-lxd-0',
'res1',
0,
'juju-lxd-0')
@mock.patch('pcmk.commit')
@mock.patch('pcmk.crm_opt_exists')
@mock.patch('pcmk.list_nodes')
def test_add_location_rules_for_local_nodes(self, list_nodes,
crm_opt_exists, commit):
existing_resources = ['loc-res1-node1']
list_nodes.return_value = ['node1', 'node2']
crm_opt_exists.side_effect = lambda x: x in existing_resources
utils.add_location_rules_for_local_nodes('res1')
commit.assert_called_once_with(
'crm -w -F configure location loc-res1-node2 res1 0: node2',
failure_is_fatal=True)
@mock.patch.object(utils, 'add_score_location_rule')
@mock.patch('pcmk.list_nodes')
def test_add_location_rules_for_pacemaker_remotes(self, list_nodes,
add_score_location_rule):
list_nodes.return_value = ['node1', 'node2', 'node3']
utils.add_location_rules_for_pacemaker_remotes([
'res1',
'res2',
'res3',
'res4',
'res5'])
expect = [
mock.call('res1', 'node1', 200),
mock.call('res1', 'node2', 0),
mock.call('res1', 'node3', 0),
mock.call('res2', 'node1', 0),
mock.call('res2', 'node2', 200),
mock.call('res2', 'node3', 0),
mock.call('res3', 'node1', 0),
mock.call('res3', 'node2', 0),
mock.call('res3', 'node3', 200),
mock.call('res4', 'node1', 200),
mock.call('res4', 'node2', 0),
mock.call('res4', 'node3', 0),
mock.call('res5', 'node1', 0),
mock.call('res5', 'node2', 200),
mock.call('res5', 'node3', 0)]
add_score_location_rule.assert_has_calls(expect)
@mock.patch('pcmk.is_resource_present')
@mock.patch('pcmk.commit')
def test_configure_pacemaker_remote(self, commit, is_resource_present):
is_resource_present.return_value = False
self.assertEqual(
utils.configure_pacemaker_remote(
'juju-aa0ba5-zaza-ed2ce6f303f0-10',
'10.0.0.10'),
'juju-aa0ba5-zaza-ed2ce6f303f0-10')
commit.assert_called_once_with(
'crm configure primitive juju-aa0ba5-zaza-ed2ce6f303f0-10 '
'ocf:pacemaker:remote params '
'server=10.0.0.10 '
'reconnect_interval=60 op monitor interval=30s',
failure_is_fatal=True)
@mock.patch('pcmk.is_resource_present')
@mock.patch('pcmk.commit')
def test_configure_pacemaker_remote_fqdn(self, commit,
is_resource_present):
is_resource_present.return_value = False
self.assertEqual(
utils.configure_pacemaker_remote(
'juju-aa0ba5-zaza-ed2ce6f303f0-10.maas',
'10.0.0.10'),
'juju-aa0ba5-zaza-ed2ce6f303f0-10.maas')
commit.assert_called_once_with(
'crm configure primitive juju-aa0ba5-zaza-ed2ce6f303f0-10.maas '
'ocf:pacemaker:remote params '
'server=10.0.0.10 '
'reconnect_interval=60 op monitor interval=30s',
failure_is_fatal=True)
@mock.patch('pcmk.is_resource_present')
@mock.patch('pcmk.commit')
def test_configure_pacemaker_remote_duplicate(self, commit,
is_resource_present):
is_resource_present.return_value = True
self.assertEqual(
utils.configure_pacemaker_remote(
'juju-aa0ba5-zaza-ed2ce6f303f0-10.maas',
'10.0.0.10'),
'juju-aa0ba5-zaza-ed2ce6f303f0-10.maas')
self.assertFalse(commit.called)
@mock.patch('pcmk.commit')
def test_cleanup_remote_nodes(self, commit):
commit.return_value = 0
utils.cleanup_remote_nodes(['res-node1', 'res-node2'])
commit_calls = [
mock.call(
'crm resource cleanup res-node1',
failure_is_fatal=False),
mock.call(
'crm resource cleanup res-node2',
failure_is_fatal=False)]
commit.assert_has_calls(commit_calls)
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
@mock.patch.object(utils, 'relation_ids')
@mock.patch.object(utils, 'add_location_rules_for_local_nodes')
@mock.patch.object(utils, 'configure_pacemaker_remote')
@mock.patch.object(utils, 'configure_maas_stonith_resource')
@mock.patch.object(utils, 'cleanup_remote_nodes')
def test_configure_pacemaker_remote_resources(
self,
cleanup_remote_nodes,
configure_maas_stonith_resource,
configure_pacemaker_remote,
add_location_rules_for_local_nodes,
relation_ids,
related_units,
relation_get):
rdata = {
'pacemaker-remote:49': {
'pacemaker-remote/0': {
'remote-hostname': '"node1"',
'remote-ip': '"10.0.0.10"',
'stonith-hostname': '"st-node1"'},
'pacemaker-remote/1': {
'remote-ip': '"10.0.0.11"',
'remote-hostname': '"node2"'},
'pacemaker-remote/2': {
'stonith-hostname': '"st-node3"'}}}
relation_ids.side_effect = lambda x: rdata.keys()
related_units.side_effect = lambda x: sorted(rdata[x].keys())
relation_get.side_effect = lambda x, y, z: rdata[z][y].get(x, None)
configure_pacemaker_remote.side_effect = \
lambda x, y: 'res-{}'.format(x)
utils.configure_pacemaker_remote_resources()
remote_calls = [
mock.call('node1', '10.0.0.10'),
mock.call('node2', '10.0.0.11')]
configure_pacemaker_remote.assert_has_calls(
remote_calls,
any_order=True)
cleanup_remote_nodes.assert_called_once_with(
['res-node1', 'res-node2'])
@mock.patch.object(utils, 'config')
@mock.patch.object(utils, 'remove_legacy_maas_stonith_resources')
@mock.patch('pcmk.commit')
@mock.patch('pcmk.is_resource_present')
def test_configure_maas_stonith_resource(self, is_resource_present,
commit, remove_legacy, config):
cfg = {
'maas_url': 'http://maas/2.0',
'maas_credentials': 'apikey'}
is_resource_present.return_value = False
config.side_effect = lambda x: cfg.get(x)
utils.configure_maas_stonith_resource(['node1'])
cmd = (
"crm configure primitive st-maas "
"stonith:external/maas "
"params url='http://maas/2.0' apikey='apikey' "
"hostnames='node1' "
"op monitor interval=25 start-delay=25 "
"timeout=25")
commit_calls = [
mock.call(cmd, failure_is_fatal=True),
]
commit.assert_has_calls(commit_calls)
@mock.patch.object(utils, 'remove_legacy_maas_stonith_resources')
@mock.patch('pcmk.commit')
@mock.patch('pcmk.is_resource_present')
def test_configure_null_stonith_resource(self, is_resource_present,
commit, remove_legacy):
is_resource_present.return_value = False
utils.configure_null_stonith_resource(['node1'])
cmd = (
"crm configure primitive st-null "
"stonith:null "
"params hostlist='node1' "
"op monitor interval=25 start-delay=25 "
"timeout=25")
commit_calls = [
mock.call(cmd, failure_is_fatal=True),
]
commit.assert_has_calls(commit_calls)
@mock.patch.object(utils, 'config')
@mock.patch.object(utils, 'remove_legacy_maas_stonith_resources')
@mock.patch('pcmk.commit')
@mock.patch('pcmk.is_resource_present')
@mock.patch('pcmk.crm_update_resource')
def test_configure_maas_stonith_resource_duplicate(self,
crm_update_resource,
is_resource_present,
commit, remove_legacy,
config):
cfg = {
'maas_url': 'http://maas/2.0',
'maas_credentials': 'apikey'}
is_resource_present.return_value = True
config.side_effect = lambda x: cfg.get(x)
utils.configure_maas_stonith_resource(['node1'])
crm_update_resource.assert_called_once_with(
'st-maas',
'stonith:external/maas',
("params url='http://maas/2.0' apikey='apikey' hostnames='node1' "
"op monitor interval=25 start-delay=25 timeout=25"))
@mock.patch.object(utils, 'config')
@mock.patch('pcmk.commit')
@mock.patch('pcmk.is_resource_present')
def test_configure_maas_stonith_resource_no_url(self,
is_resource_present,
commit, config):
cfg = {
'maas_credentials': 'apikey'}
is_resource_present.return_value = False
config.side_effect = lambda x: cfg.get(x)
with self.assertRaises(ValueError):
utils.configure_maas_stonith_resource('node1')
@mock.patch.object(utils, 'unit_get')
@mock.patch.object(utils, 'get_ipv6_addr')
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
@mock.patch.object(utils, 'relation_ids')
@mock.patch.object(utils, 'config')
def test_get_node_flags(self, config, relation_ids, related_units,
relation_get, get_ipv6_addr, unit_get):
cfg = {}
config.side_effect = lambda x: cfg.get(x)
unit_get.return_value = '10.0.0.41'
relation_ids.return_value = ['relid1']
related_units.return_value = ['unit1', 'unit2', 'unit3']
rdata = {
'unit1': {
'random_flag': True,
'private-address': '10.0.0.10'},
'unit2': {
'random_flag': True,
'private-address': '10.0.0.34'},
'unit3': {
'random_flag': True,
'private-address': '10.0.0.16'}}
relation_get.side_effect = lambda v, rid, unit: rdata[unit].get(v)
rget_calls = [
mock.call('random_flag', rid='relid1', unit='unit1'),
mock.call('private-address', rid='relid1', unit='unit1'),
mock.call('random_flag', rid='relid1', unit='unit2'),
mock.call('private-address', rid='relid1', unit='unit2'),
mock.call('random_flag', rid='relid1', unit='unit3'),
mock.call('private-address', rid='relid1', unit='unit3')]
self.assertSequenceEqual(
utils.get_node_flags('random_flag'),
['10.0.0.10', '10.0.0.16', '10.0.0.34', '10.0.0.41'])
relation_get.assert_has_calls(rget_calls)
@mock.patch.object(utils, 'get_node_flags')
def test_get_cluster_nodes(self, get_node_flags):
utils.get_cluster_nodes()
get_node_flags.assert_called_once_with('ready')
@mock.patch.object(utils, 'get_node_flags')
def test_get_member_ready_nodes(self, get_node_flags):
utils.get_member_ready_nodes()
get_node_flags.assert_called_once_with('member_ready')
@mock.patch('pcmk.commit')
@mock.patch('pcmk.list_nodes')
@mock.patch.object(utils, 'add_location_rules_for_local_nodes')
@mock.patch.object(utils, 'need_resources_on_remotes')
def test_configure_resources_on_remotes(self, need_resources_on_remotes,
add_location_rules_for_local_nodes,
list_nodes, commit):
list_nodes.return_value = ['node1', 'node2', 'node3']
need_resources_on_remotes.return_value = False
clones = {
'cl_res_masakari_haproxy': u'res_masakari_haproxy'}
resources = {
'res_masakari_1e39e82_vip': u'ocf:heartbeat:IPaddr2',
'res_masakari_flump': u'ocf:heartbeat:IPaddr2',
'res_masakari_haproxy': u'lsb:haproxy'}
groups = {
'grp_masakari_vips': 'res_masakari_1e39e82_vip'}
utils.configure_resources_on_remotes(
resources=resources,
clones=clones,
groups=groups)
add_loc_calls = [
mock.call('cl_res_masakari_haproxy'),
mock.call('res_masakari_flump'),
mock.call('grp_masakari_vips')]
add_location_rules_for_local_nodes.assert_has_calls(
add_loc_calls,
any_order=True)
commit.assert_called_once_with(
'crm_resource --resource cl_res_masakari_haproxy '
'--set-parameter clone-max '
'--meta --parameter-value 3',
failure_is_fatal=True)
@mock.patch('pcmk.commit')
@mock.patch('pcmk.list_nodes')
@mock.patch.object(utils, 'add_location_rules_for_local_nodes')
@mock.patch.object(utils, 'need_resources_on_remotes')
def test_configure_resources_on_remotes_true(
self,
need_resources_on_remotes,
add_location_rules_for_local_nodes,
list_nodes,
commit):
list_nodes.return_value = ['node1', 'node2', 'node3']
need_resources_on_remotes.return_value = True
clones = {
'cl_res_masakari_haproxy': u'res_masakari_haproxy'}
resources = {
'res_masakari_1e39e82_vip': u'ocf:heartbeat:IPaddr2',
'res_masakari_flump': u'ocf:heartbeat:IPaddr2',
'res_masakari_haproxy': u'lsb:haproxy'}
groups = {
'grp_masakari_vips': 'res_masakari_1e39e82_vip'}
utils.configure_resources_on_remotes(
resources=resources,
clones=clones,
groups=groups)
self.assertFalse(commit.called)
@mock.patch('pcmk.commit')
@mock.patch('pcmk.list_nodes')
@mock.patch.object(utils, 'add_location_rules_for_local_nodes')
@mock.patch.object(utils, 'need_resources_on_remotes')
def test_configure_resources_on_remotes_unknown(
self,
need_resources_on_remotes,
add_location_rules_for_local_nodes,
list_nodes,
commit):
list_nodes.return_value = ['node1', 'node2', 'node3']
need_resources_on_remotes.side_effect = ValueError
clones = {
'cl_res_masakari_haproxy': u'res_masakari_haproxy'}
resources = {
'res_masakari_1e39e82_vip': u'ocf:heartbeat:IPaddr2',
'res_masakari_flump': u'ocf:heartbeat:IPaddr2',
'res_masakari_haproxy': u'lsb:haproxy'}
groups = {
'grp_masakari_vips': 'res_masakari_1e39e82_vip'}
utils.configure_resources_on_remotes(
resources=resources,
clones=clones,
groups=groups)
self.assertFalse(commit.called)
@mock.patch('pcmk.commit')
def test_configure_global_cluster(self, mock_commit):
utils.configure_cluster_global(240)
mock_commit.assert_any_call('crm configure rsc_defaults '
'$id="rsc-options" '
'resource-stickiness="100" '
'failure-timeout=240')
class MockHookData(object):
class MockDB(object):
def __init__(self):
self.kv = {}
def set(self, key, value):
self.kv[key] = value
def get(self, key):
return self.kv.get(key)
def __init__(self):
self.kv = self.MockDB()
@contextlib.contextmanager
def __call__(self):
yield [self.kv]
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
return
@mock.patch.object(utils.unitdata, 'HookData')
def test_set_waiting_unit_series_upgrade(self, HookData):
hook_data = self.MockHookData()
HookData.return_value = hook_data
utils.set_waiting_unit_series_upgrade()
self.assertTrue(hook_data.kv.get('waiting-unit-series-upgrade'))
@mock.patch.object(utils.unitdata, 'HookData')
def test_clear_waiting_unit_series_upgrade(self, HookData):
hook_data = self.MockHookData()
HookData.return_value = hook_data
utils.set_waiting_unit_series_upgrade()
self.assertTrue(hook_data.kv.get('waiting-unit-series-upgrade'))
utils.clear_waiting_unit_series_upgrade()
self.assertFalse(hook_data.kv.get('waiting-unit-series-upgrade'))
@mock.patch.object(utils.unitdata, 'HookData')
def test_is_waiting_unit_series_upgrade_set(self, hookdata):
hook_data = self.MockHookData()
hookdata.return_value = hook_data
# false if key is absent
self.assertFalse(utils.is_waiting_unit_series_upgrade_set())
# True if waiting-unit-upgrade het been set
utils.set_waiting_unit_series_upgrade()
self.assertTrue(utils.is_waiting_unit_series_upgrade_set())
# False if waiting-unit-upgrade has been cleared
utils.clear_waiting_unit_series_upgrade()
self.assertFalse(utils.is_waiting_unit_series_upgrade_set())
@mock.patch.object(utils, 'relation_get')
@mock.patch.object(utils, 'related_units')
def test_get_series_upgrade_notifications(self, related_units,
relation_get):
related_units.return_value = ['unit1', 'unit2']
rdata = {
'unit1': {
'series_upgrade_of_unit1': 'trusty'},
'unit2': {}}
relation_get.side_effect = lambda rid, unit: rdata[unit]
self.assertEqual(
utils.get_series_upgrade_notifications('rid1'),
{'unit1': 'trusty'})
@mock.patch.object(utils, 'service_stop')
@mock.patch.object(utils, 'service_running')
@mock.patch.object(utils, 'disable_lsb_services')
def test_disable_ha_services(self, disable_lsb_services, service_running,
service_stop):
disable_calls = [
mock.call('corosync'),
mock.call('pacemaker')
]
stop_calls = [
mock.call('corosync'),
mock.call('pacemaker')
]
service_running.return_value = True
utils.disable_ha_services()
disable_lsb_services.assert_has_calls(disable_calls)
service_stop.assert_has_calls(stop_calls)
@mock.patch.object(utils, 'service_start')
@mock.patch.object(utils, 'service_running')
@mock.patch.object(utils, 'enable_lsb_services')
def test_enable_ha_services(self, enable_lsb_services, service_running,
service_start):
enable_calls = [
mock.call('pacemaker'),
mock.call('corosync')
]
start_calls = [
mock.call('pacemaker'),
mock.call('corosync')
]
service_running.return_value = False
utils.enable_ha_services()
enable_lsb_services.assert_has_calls(enable_calls)
service_start.assert_has_calls(start_calls)
@mock.patch.object(utils, 'local_unit')
def test_get_series_upgrade_key(self, local_unit):
local_unit.return_value = 'nova-cloud-controller/2'
self.assertEqual(
utils.get_series_upgrade_key(),
'series_upgrade_of_nova_cloud_controller_2'
)
@mock.patch.object(utils, 'relation_set')
@mock.patch.object(utils, 'relation_ids')
@mock.patch.object(utils, 'local_unit')
@mock.patch.object(utils, 'lsb_release')
def test_notify_peers_of_series_upgrade(self, lsb_release, local_unit,
relation_ids, relation_set):
lsb_release.return_value = {
'DISTRIB_CODENAME': 'trusty'}
local_unit.return_value = 'nova-compute/2'
relation_ids.return_value = ['rid1']
utils.notify_peers_of_series_upgrade()
relation_set.assert_called_once_with(
relation_id='rid1',
relation_settings={'series_upgrade_of_nova_compute_2': 'trusty'})
@mock.patch.object(utils, 'relation_set')
@mock.patch.object(utils, 'relation_ids')
@mock.patch.object(utils, 'local_unit')
def test_clear_series_upgrade_notification(self, local_unit, relation_ids,
relation_set):
local_unit.return_value = 'nova-compute/2'
relation_ids.return_value = ['rid1']
utils.clear_series_upgrade_notification()
relation_set.assert_called_once_with(
relation_id='rid1',
relation_settings={'series_upgrade_of_nova_compute_2': None})
@mock.patch('pcmk.crm_maas_stonith_resource_list')
@mock.patch('pcmk.commit')
def test_remove_legacy_maas_stonith_resources(self, mock_commit,
mock_resource_list):
mock_resource_list.return_value = ['st-maas-abcd', 'st-maas-1234']
utils.remove_legacy_maas_stonith_resources()
commit_calls = [
mock.call('crm -w -F resource stop st-maas-abcd'),
mock.call('crm -w -F configure delete st-maas-abcd'),
mock.call('crm -w -F resource stop st-maas-1234'),
mock.call('crm -w -F configure delete st-maas-1234')]
mock_commit.assert_has_calls(commit_calls)
@mock.patch.object(utils, 'leader_set')
def test_set_stonith_configured(self, leader_set):
utils.set_stonith_configured(True)
leader_set.assert_called_once_with(
{'stonith-configured': True})
@mock.patch.object(utils, 'leader_get')
def test_is_stonith_configured(self, leader_get):
leader_get.return_value = 'True'
self.assertTrue(
utils.is_stonith_configured())
leader_get.return_value = 'False'
self.assertFalse(
utils.is_stonith_configured())
leader_get.return_value = ''
self.assertFalse(
utils.is_stonith_configured())
@mock.patch('pcmk.commit')
def test_enable_stonith(self, commit):
utils.enable_stonith()
commit.assert_called_once_with(
'crm configure property stonith-enabled=true',
failure_is_fatal=True)
@mock.patch('pcmk.commit')
def test_disable_stonith(self, commit):
utils.disable_stonith()
commit.assert_called_once_with(
'crm configure property stonith-enabled=false',
failure_is_fatal=True)