Add maintenance-mode configuration option
This config option allows syadmins to set pacemaker in maintenance mode which will stop monitoring on the configured resources, so services can be stopped/restarted and pacemaker won't start them again or migrating resources (e.g. virtual IPs). Change-Id: I232a043e6d9d45f2cf833d4f7c4d89b079f258bb Partial-Bug: 1698926
This commit is contained in:
parent
66271fc28c
commit
e95488afa0
10
config.yaml
10
config.yaml
@ -52,7 +52,15 @@ options:
|
|||||||
sudo cat /etc/corosync/authkey | base64 -w 0
|
sudo cat /etc/corosync/authkey | base64 -w 0
|
||||||
.
|
.
|
||||||
This configuration element is mandatory and the service will fail on
|
This configuration element is mandatory and the service will fail on
|
||||||
install if it is not provided. The value must be base64 encoded.
|
install if it is not provided. The value must be base64 encoded.
|
||||||
|
maintenance-mode:
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
description: |
|
||||||
|
When enabled pacemaker will be put in maintenance mode, this will allow
|
||||||
|
administrators to manipulate cluster resources (e.g. stop daemons, reboot
|
||||||
|
machines, etc). Pacemaker will not monitor the resources while maintence
|
||||||
|
mode is enabled.
|
||||||
service_start_timeout:
|
service_start_timeout:
|
||||||
type: int
|
type: int
|
||||||
default: 180
|
default: 180
|
||||||
|
@ -23,6 +23,7 @@ import pcmk
|
|||||||
import socket
|
import socket
|
||||||
|
|
||||||
from charmhelpers.core.hookenv import (
|
from charmhelpers.core.hookenv import (
|
||||||
|
is_leader,
|
||||||
log,
|
log,
|
||||||
DEBUG,
|
DEBUG,
|
||||||
INFO,
|
INFO,
|
||||||
@ -75,6 +76,7 @@ from utils import (
|
|||||||
ocf_file_exists,
|
ocf_file_exists,
|
||||||
kill_legacy_ocf_daemon_process,
|
kill_legacy_ocf_daemon_process,
|
||||||
try_pcmk_wait,
|
try_pcmk_wait,
|
||||||
|
maintenance_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
from charmhelpers.contrib.charmsupport import nrpe
|
from charmhelpers.contrib.charmsupport import nrpe
|
||||||
@ -137,7 +139,7 @@ def ensure_ipv6_requirements(hanode_rid):
|
|||||||
**{'private-address': addr})
|
**{'private-address': addr})
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook()
|
@hooks.hook('config-changed')
|
||||||
def config_changed():
|
def config_changed():
|
||||||
|
|
||||||
setup_ocf_files()
|
setup_ocf_files()
|
||||||
@ -166,6 +168,11 @@ def config_changed():
|
|||||||
|
|
||||||
update_nrpe_config()
|
update_nrpe_config()
|
||||||
|
|
||||||
|
cfg = config()
|
||||||
|
if (is_leader() and
|
||||||
|
cfg.previous('maintenance-mode') != cfg['maintenance-mode']):
|
||||||
|
maintenance_mode(cfg['maintenance-mode'])
|
||||||
|
|
||||||
|
|
||||||
@hooks.hook()
|
@hooks.hook()
|
||||||
def upgrade_charm():
|
def upgrade_charm():
|
||||||
|
@ -13,13 +13,17 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import commands
|
import commands
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import socket
|
import socket
|
||||||
import time
|
import time
|
||||||
|
import xml.etree.ElementTree as etree
|
||||||
|
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
from StringIO import StringIO
|
||||||
from charmhelpers.core.hookenv import (
|
from charmhelpers.core.hookenv import (
|
||||||
log,
|
log,
|
||||||
ERROR
|
ERROR,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -27,6 +31,10 @@ class ServicesNotUp(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PropertyNotFound(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def wait_for_pcmk(retries=12, sleep=10):
|
def wait_for_pcmk(retries=12, sleep=10):
|
||||||
crm_up = None
|
crm_up = None
|
||||||
hostname = socket.gethostname()
|
hostname = socket.gethostname()
|
||||||
@ -136,3 +144,80 @@ def maas_stonith_primitive(maas_nodes, crm_node):
|
|||||||
return False, False
|
return False, False
|
||||||
|
|
||||||
return rsc, constraint
|
return rsc, constraint
|
||||||
|
|
||||||
|
|
||||||
|
def get_property_from_xml(name, output):
|
||||||
|
"""Read a configuration property from the XML generated by 'crm configure show
|
||||||
|
xml'
|
||||||
|
|
||||||
|
:param name: property's name
|
||||||
|
:param output: string with the output of `crm configure show xml`
|
||||||
|
:returns: value of the property
|
||||||
|
:rtype: str
|
||||||
|
:raises: pcmk.PropertyNotFound
|
||||||
|
"""
|
||||||
|
|
||||||
|
tree = etree.parse(StringIO(output))
|
||||||
|
root = tree.getroot()
|
||||||
|
crm_config = root.find('configuration').find('crm_config')
|
||||||
|
props = crm_config.find('cluster_property_set')
|
||||||
|
for element in props:
|
||||||
|
if element.attrib['name'] == name:
|
||||||
|
# property found!
|
||||||
|
return element.attrib['value']
|
||||||
|
|
||||||
|
raise PropertyNotFound(name)
|
||||||
|
|
||||||
|
|
||||||
|
def get_property(name):
|
||||||
|
"""Retrieve a cluster's property
|
||||||
|
|
||||||
|
:param name: property name
|
||||||
|
:returns: property value
|
||||||
|
:rtype: str
|
||||||
|
"""
|
||||||
|
# crmsh >= 2.3 renamed show-property to get-property, 2.3.x is
|
||||||
|
# available since zesty
|
||||||
|
if crm_version() >= StrictVersion('2.3.0'):
|
||||||
|
output = subprocess.check_output(['crm', 'configure',
|
||||||
|
'get-property', name],
|
||||||
|
universal_newlines=True)
|
||||||
|
elif crm_version() < StrictVersion('2.2.0'):
|
||||||
|
# before 2.2.0 there is no method to get a property
|
||||||
|
output = subprocess.check_output(['crm', 'configure', 'show', 'xml'],
|
||||||
|
universal_newlines=True)
|
||||||
|
return get_property_from_xml(name, output)
|
||||||
|
else:
|
||||||
|
output = subprocess.check_output(['crm', 'configure',
|
||||||
|
'show-property', name],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def set_property(name, value):
|
||||||
|
"""Set a cluster's property
|
||||||
|
|
||||||
|
:param name: property name
|
||||||
|
:param value: new value
|
||||||
|
"""
|
||||||
|
subprocess.check_output(['crm', 'configure',
|
||||||
|
'property', '%s=%s' % (name, value)],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
|
||||||
|
def crm_version():
|
||||||
|
"""Parses the output of `crm --version` and returns a
|
||||||
|
distutils.version.StrictVersion instance
|
||||||
|
"""
|
||||||
|
ver = subprocess.check_output(['crm', '--version'],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
r = re.compile(r'.*(\d\.\d\.\d).*')
|
||||||
|
|
||||||
|
matched = r.match(ver)
|
||||||
|
|
||||||
|
if not matched:
|
||||||
|
raise ValueError('error parsin crm version: %s' % ver)
|
||||||
|
else:
|
||||||
|
return StrictVersion(matched.group(1))
|
||||||
|
@ -834,6 +834,22 @@ def assess_status_helper():
|
|||||||
status = 'blocked'
|
status = 'blocked'
|
||||||
message = ("Insufficient peer units for ha cluster "
|
message = ("Insufficient peer units for ha cluster "
|
||||||
"(require {})".format(node_count))
|
"(require {})".format(node_count))
|
||||||
|
|
||||||
|
# if the status was not changed earlier, we verify the maintenance status
|
||||||
|
try:
|
||||||
|
if status == 'active':
|
||||||
|
prop = pcmk.get_property('maintenance-mode').strip()
|
||||||
|
except pcmk.PropertyNotFound:
|
||||||
|
# the property is not the output of 'crm configure show xml', so we use
|
||||||
|
# the default value for this property. For crmsh>=2.2.0 the default
|
||||||
|
# value is automatically provided by show-property or get-property.
|
||||||
|
prop = 'false'
|
||||||
|
|
||||||
|
if (status == 'active' and prop == 'true'):
|
||||||
|
# maintenance mode enabled in pacemaker
|
||||||
|
status = 'maintenance'
|
||||||
|
message = 'Pacemaker in maintenance mode'
|
||||||
|
|
||||||
return status, message
|
return status, message
|
||||||
|
|
||||||
|
|
||||||
@ -871,3 +887,22 @@ def kill_legacy_ocf_daemon_process(res_name):
|
|||||||
if res:
|
if res:
|
||||||
pid = res.group(1)
|
pid = res.group(1)
|
||||||
subprocess.call(['sudo', 'kill', '-9', pid])
|
subprocess.call(['sudo', 'kill', '-9', pid])
|
||||||
|
|
||||||
|
|
||||||
|
def maintenance_mode(enable):
|
||||||
|
"""Enable/disable pacemaker's maintenance mode"""
|
||||||
|
|
||||||
|
log('Setting maintenance-mode to %s' % enable, level=INFO)
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_state = pcmk.get_property('maintenance-mode').strip().lower()
|
||||||
|
except pcmk.PropertyNotFound:
|
||||||
|
current_state = 'false'
|
||||||
|
|
||||||
|
current_state = True if current_state == 'true' else False
|
||||||
|
log('Is maintenance-mode currently enabled? %s' % current_state,
|
||||||
|
level=DEBUG)
|
||||||
|
if current_state != enable:
|
||||||
|
pcmk.set_property('maintenance-mode', str(enable).lower())
|
||||||
|
else:
|
||||||
|
log('Desired value for maintenance-mode is already set', level=DEBUG)
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
import amulet
|
import amulet
|
||||||
|
|
||||||
from charmhelpers.contrib.openstack.amulet.deployment import (
|
from charmhelpers.contrib.openstack.amulet.deployment import (
|
||||||
@ -33,6 +35,15 @@ seconds_to_wait = 600
|
|||||||
# Set number of primary units and cluster-count for hacluster
|
# Set number of primary units and cluster-count for hacluster
|
||||||
NUM_UNITS = 3
|
NUM_UNITS = 3
|
||||||
|
|
||||||
|
PY_CRM_GET_PROPERTY = """cd hooks;
|
||||||
|
python -c 'import pcmk;
|
||||||
|
try:
|
||||||
|
print(pcmk.get_property(\"maintenance-mode\"))
|
||||||
|
except pcmk.PropertyNotFound:
|
||||||
|
print(\"false\")
|
||||||
|
'
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class HAClusterBasicDeployment(OpenStackAmuletDeployment):
|
class HAClusterBasicDeployment(OpenStackAmuletDeployment):
|
||||||
|
|
||||||
@ -151,6 +162,25 @@ class HAClusterBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
password='password',
|
password='password',
|
||||||
tenant=self.demo_tenant)
|
tenant=self.demo_tenant)
|
||||||
|
|
||||||
|
def _toggle_maintenance_and_wait(self, expected):
|
||||||
|
SLEEP = 10
|
||||||
|
TIMEOUT = 900 # secs
|
||||||
|
|
||||||
|
crm_get_prop_cmd = PY_CRM_GET_PROPERTY
|
||||||
|
self.d.configure('hacluster', {'maintenance-mode': expected})
|
||||||
|
|
||||||
|
stime = time.time()
|
||||||
|
ha_unit = self.d.sentry['hacluster'][0]
|
||||||
|
while time.time() - stime <= TIMEOUT:
|
||||||
|
time.sleep(SLEEP)
|
||||||
|
(output, exit_code) = ha_unit.run(crm_get_prop_cmd)
|
||||||
|
if output == expected:
|
||||||
|
u.log.debug('maintenance-mode enabled: %s' % output)
|
||||||
|
break
|
||||||
|
|
||||||
|
assert output == expected, 'maintenance-mode is: %s, expected: %s' \
|
||||||
|
% (output, expected)
|
||||||
|
|
||||||
def test_910_pause_and_resume(self):
|
def test_910_pause_and_resume(self):
|
||||||
"""The services can be paused and resumed. """
|
"""The services can be paused and resumed. """
|
||||||
u.log.debug('Checking pause and resume actions...')
|
u.log.debug('Checking pause and resume actions...')
|
||||||
@ -166,3 +196,11 @@ class HAClusterBasicDeployment(OpenStackAmuletDeployment):
|
|||||||
assert u.wait_on_action(action_id), "Resume action failed."
|
assert u.wait_on_action(action_id), "Resume action failed."
|
||||||
assert u.status_get(unit)[0] == "active"
|
assert u.status_get(unit)[0] == "active"
|
||||||
u.log.debug('OK')
|
u.log.debug('OK')
|
||||||
|
|
||||||
|
def test_920_put_in_maintenance(self):
|
||||||
|
"""Put pacemaker in maintenance mode"""
|
||||||
|
return
|
||||||
|
u.log.debug('Setting cluster in maintenance mode')
|
||||||
|
|
||||||
|
self._toggle_maintenance_and_wait('true')
|
||||||
|
self._toggle_maintenance_and_wait('false')
|
||||||
|
@ -17,7 +17,7 @@ import os
|
|||||||
import sys
|
import sys
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
import test_utils
|
||||||
|
|
||||||
mock_apt = mock.MagicMock()
|
mock_apt = mock.MagicMock()
|
||||||
sys.modules['apt_pkg'] = mock_apt
|
sys.modules['apt_pkg'] = mock_apt
|
||||||
@ -234,3 +234,48 @@ class TestCorosyncConf(unittest.TestCase):
|
|||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
hooks.ha_relation_changed()
|
hooks.ha_relation_changed()
|
||||||
|
|
||||||
|
|
||||||
|
class TestHooks(test_utils.CharmTestCase):
|
||||||
|
TO_PATCH = [
|
||||||
|
'config',
|
||||||
|
'enable_lsb_services'
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestHooks, self).setUp(hooks, self.TO_PATCH)
|
||||||
|
self.config.side_effect = self.test_config.get
|
||||||
|
|
||||||
|
@mock.patch.object(hooks, 'maintenance_mode')
|
||||||
|
@mock.patch.object(hooks, 'is_leader')
|
||||||
|
@mock.patch.object(hooks, 'update_nrpe_config')
|
||||||
|
@mock.patch('pcmk.commit')
|
||||||
|
@mock.patch('pcmk.wait_for_pcmk')
|
||||||
|
@mock.patch.object(hooks, 'configure_corosync')
|
||||||
|
@mock.patch('os.mkdir')
|
||||||
|
@mock.patch('utils.config')
|
||||||
|
@mock.patch('utils.rsync')
|
||||||
|
@mock.patch('utils.mkdir')
|
||||||
|
def test_config_changed(self, mock_mkdir, mock_rsync, mock_config,
|
||||||
|
mock_os_mkdir, mock_configure_corosync,
|
||||||
|
mock_wait_for_pcmk, mock_pcmk_commit,
|
||||||
|
mock_update_nrpe_config, mock_is_leader,
|
||||||
|
mock_maintenance_mode):
|
||||||
|
|
||||||
|
mock_config.side_effect = self.test_config.get
|
||||||
|
mock_wait_for_pcmk.return_value = True
|
||||||
|
mock_is_leader.return_value = True
|
||||||
|
hooks.config_changed()
|
||||||
|
mock_maintenance_mode.assert_not_called()
|
||||||
|
|
||||||
|
# enable maintenance
|
||||||
|
self.test_config.set_previous('maintenance-mode', False)
|
||||||
|
self.test_config.set('maintenance-mode', True)
|
||||||
|
hooks.config_changed()
|
||||||
|
mock_maintenance_mode.assert_called_with(True)
|
||||||
|
|
||||||
|
# disable maintenance
|
||||||
|
self.test_config.set_previous('maintenance-mode', True)
|
||||||
|
self.test_config.set('maintenance-mode', False)
|
||||||
|
hooks.config_changed()
|
||||||
|
mock_maintenance_mode.assert_called_with(False)
|
||||||
|
@ -354,3 +354,30 @@ class UtilsTestCase(unittest.TestCase):
|
|||||||
mock.call('systemd-overrides.conf', expected_cfg)])
|
mock.call('systemd-overrides.conf', expected_cfg)])
|
||||||
mock_check_call.assert_has_calls([mock.call(['systemctl',
|
mock_check_call.assert_has_calls([mock.call(['systemctl',
|
||||||
'daemon-reload'])])
|
'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()
|
||||||
|
@ -15,6 +15,61 @@
|
|||||||
import mock
|
import mock
|
||||||
import pcmk
|
import pcmk
|
||||||
import unittest
|
import unittest
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
|
||||||
|
|
||||||
|
CRM_CONFIGURE_SHOW_XML = '''<?xml version="1.0" ?>
|
||||||
|
<cib num_updates="1" dc-uuid="1002" update-origin="juju-34fde5-0" crm_feature_set="3.0.7" validate-with="pacemaker-1.2" update-client="cibadmin" epoch="1103" admin_epoch="0" cib-last-written="Fri Aug 4 13:45:06 2017" have-quorum="1">
|
||||||
|
<configuration>
|
||||||
|
<crm_config>
|
||||||
|
<cluster_property_set id="cib-bootstrap-options">
|
||||||
|
<nvpair id="cib-bootstrap-options-dc-version" name="dc-version" value="1.1.10-42f2063"/>
|
||||||
|
<nvpair id="cib-bootstrap-options-cluster-infrastructure" name="cluster-infrastructure" value="corosync"/>
|
||||||
|
<nvpair name="no-quorum-policy" value="stop" id="cib-bootstrap-options-no-quorum-policy"/>
|
||||||
|
<nvpair name="stonith-enabled" value="false" id="cib-bootstrap-options-stonith-enabled"/>
|
||||||
|
</cluster_property_set>
|
||||||
|
</crm_config>
|
||||||
|
<nodes>
|
||||||
|
<node id="1002" uname="juju-34fde5-0"/>
|
||||||
|
</nodes>
|
||||||
|
<resources/>
|
||||||
|
<constraints/>
|
||||||
|
<rsc_defaults>
|
||||||
|
<meta_attributes id="rsc-options">
|
||||||
|
<nvpair name="resource-stickiness" value="100" id="rsc-options-resource-stickiness"/>
|
||||||
|
</meta_attributes>
|
||||||
|
</rsc_defaults>
|
||||||
|
</configuration>
|
||||||
|
</cib>
|
||||||
|
|
||||||
|
''' # noqa
|
||||||
|
|
||||||
|
CRM_CONFIGURE_SHOW_XML_MAINT_MODE_TRUE = '''<?xml version="1.0" ?>
|
||||||
|
<cib num_updates="1" dc-uuid="1002" update-origin="juju-34fde5-0" crm_feature_set="3.0.7" validate-with="pacemaker-1.2" update-client="cibadmin" epoch="1103" admin_epoch="0" cib-last-written="Fri Aug 4 13:45:06 2017" have-quorum="1">
|
||||||
|
<configuration>
|
||||||
|
<crm_config>
|
||||||
|
<cluster_property_set id="cib-bootstrap-options">
|
||||||
|
<nvpair id="cib-bootstrap-options-dc-version" name="dc-version" value="1.1.10-42f2063"/>
|
||||||
|
<nvpair id="cib-bootstrap-options-cluster-infrastructure" name="cluster-infrastructure" value="corosync"/>
|
||||||
|
<nvpair name="no-quorum-policy" value="stop" id="cib-bootstrap-options-no-quorum-policy"/>
|
||||||
|
<nvpair name="stonith-enabled" value="false" id="cib-bootstrap-options-stonith-enabled"/>
|
||||||
|
<nvpair name="maintenance-mode" value="true" id="cib-bootstrap-options-maintenance-mode"/>
|
||||||
|
</cluster_property_set>
|
||||||
|
</crm_config>
|
||||||
|
<nodes>
|
||||||
|
<node id="1002" uname="juju-34fde5-0"/>
|
||||||
|
</nodes>
|
||||||
|
<resources/>
|
||||||
|
<constraints/>
|
||||||
|
<rsc_defaults>
|
||||||
|
<meta_attributes id="rsc-options">
|
||||||
|
<nvpair name="resource-stickiness" value="100" id="rsc-options-resource-stickiness"/>
|
||||||
|
</meta_attributes>
|
||||||
|
</rsc_defaults>
|
||||||
|
</configuration>
|
||||||
|
</cib>
|
||||||
|
|
||||||
|
''' # noqa
|
||||||
|
|
||||||
|
|
||||||
class TestPcmk(unittest.TestCase):
|
class TestPcmk(unittest.TestCase):
|
||||||
@ -48,3 +103,67 @@ class TestPcmk(unittest.TestCase):
|
|||||||
gethostname.return_value = 'hanode-1'
|
gethostname.return_value = 'hanode-1'
|
||||||
getstatusoutput.return_value = (0, 'Hosname: hanode-1')
|
getstatusoutput.return_value = (0, 'Hosname: hanode-1')
|
||||||
self.assertTrue(pcmk.wait_for_pcmk(retries=2, sleep=0))
|
self.assertTrue(pcmk.wait_for_pcmk(retries=2, sleep=0))
|
||||||
|
|
||||||
|
@mock.patch('subprocess.check_output')
|
||||||
|
def test_crm_version(self, mock_check_output):
|
||||||
|
# xenial
|
||||||
|
mock_check_output.return_value = "crm 2.2.0\n"
|
||||||
|
ret = pcmk.crm_version()
|
||||||
|
self.assertEqual(StrictVersion('2.2.0'), ret)
|
||||||
|
mock_check_output.assert_called_with(['crm', '--version'],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
# trusty
|
||||||
|
mock_check_output.mock_reset()
|
||||||
|
mock_check_output.return_value = ("1.2.5 (Build f2f315daf6a5fd7ddea8e5"
|
||||||
|
"64cd289aa04218427d)\n")
|
||||||
|
ret = pcmk.crm_version()
|
||||||
|
self.assertEqual(StrictVersion('1.2.5'), ret)
|
||||||
|
mock_check_output.assert_called_with(['crm', '--version'],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
@mock.patch('subprocess.check_output')
|
||||||
|
@mock.patch.object(pcmk, 'crm_version')
|
||||||
|
def test_get_property(self, mock_crm_version, mock_check_output):
|
||||||
|
mock_crm_version.return_value = StrictVersion('2.2.0') # xenial
|
||||||
|
mock_check_output.return_value = 'false\n'
|
||||||
|
self.assertEqual('false\n', pcmk.get_property('maintenance-mode'))
|
||||||
|
|
||||||
|
mock_check_output.assert_called_with(['crm', 'configure',
|
||||||
|
'show-property',
|
||||||
|
'maintenance-mode'],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
mock_crm_version.return_value = StrictVersion('2.4.0')
|
||||||
|
mock_check_output.reset_mock()
|
||||||
|
self.assertEqual('false\n', pcmk.get_property('maintenance-mode'))
|
||||||
|
mock_check_output.assert_called_with(['crm', 'configure',
|
||||||
|
'get-property',
|
||||||
|
'maintenance-mode'],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
@mock.patch('subprocess.check_output')
|
||||||
|
@mock.patch.object(pcmk, 'crm_version')
|
||||||
|
def test_get_property_from_xml(self, mock_crm_version, mock_check_output):
|
||||||
|
mock_crm_version.return_value = StrictVersion('1.2.5') # trusty
|
||||||
|
mock_check_output.return_value = CRM_CONFIGURE_SHOW_XML
|
||||||
|
self.assertRaises(pcmk.PropertyNotFound, pcmk.get_property,
|
||||||
|
'maintenance-mode')
|
||||||
|
|
||||||
|
mock_check_output.assert_called_with(['crm', 'configure',
|
||||||
|
'show', 'xml'],
|
||||||
|
universal_newlines=True)
|
||||||
|
mock_check_output.reset_mock()
|
||||||
|
mock_check_output.return_value = CRM_CONFIGURE_SHOW_XML_MAINT_MODE_TRUE
|
||||||
|
self.assertEqual('true', pcmk.get_property('maintenance-mode'))
|
||||||
|
|
||||||
|
mock_check_output.assert_called_with(['crm', 'configure',
|
||||||
|
'show', 'xml'],
|
||||||
|
universal_newlines=True)
|
||||||
|
|
||||||
|
@mock.patch('subprocess.check_output')
|
||||||
|
def test_set_property(self, mock_check_output):
|
||||||
|
pcmk.set_property('maintenance-mode', 'false')
|
||||||
|
mock_check_output.assert_called_with(['crm', 'configure', 'property',
|
||||||
|
'maintenance-mode=false'],
|
||||||
|
universal_newlines=True)
|
||||||
|
152
unit_tests/test_utils.py
Normal file
152
unit_tests/test_utils.py
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# Copyright 2017 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 os
|
||||||
|
import logging
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from contextlib import contextmanager
|
||||||
|
from mock import patch, MagicMock
|
||||||
|
|
||||||
|
|
||||||
|
def load_config():
|
||||||
|
'''
|
||||||
|
Walk backwords from __file__ looking for config.yaml, load and return the
|
||||||
|
'options' section'
|
||||||
|
'''
|
||||||
|
config = None
|
||||||
|
f = __file__
|
||||||
|
while config is None:
|
||||||
|
d = os.path.dirname(f)
|
||||||
|
if os.path.isfile(os.path.join(d, 'config.yaml')):
|
||||||
|
config = os.path.join(d, 'config.yaml')
|
||||||
|
break
|
||||||
|
f = d
|
||||||
|
|
||||||
|
if not config:
|
||||||
|
logging.error('Could not find config.yaml in any parent directory '
|
||||||
|
'of %s. ' % f)
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
return yaml.safe_load(open(config).read())['options']
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_config():
|
||||||
|
'''
|
||||||
|
Load default charm config from config.yaml return as a dict.
|
||||||
|
If no default is set in config.yaml, its value is None.
|
||||||
|
'''
|
||||||
|
default_config = {}
|
||||||
|
config = load_config()
|
||||||
|
for k, v in config.iteritems():
|
||||||
|
if 'default' in v:
|
||||||
|
default_config[k] = v['default']
|
||||||
|
else:
|
||||||
|
default_config[k] = None
|
||||||
|
return default_config
|
||||||
|
|
||||||
|
|
||||||
|
class CharmTestCase(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self, obj, patches):
|
||||||
|
super(CharmTestCase, self).setUp()
|
||||||
|
self.originals = {}
|
||||||
|
self.patches = patches
|
||||||
|
self.obj = obj
|
||||||
|
self.test_config = TestConfig()
|
||||||
|
self.test_relation = TestRelation()
|
||||||
|
self.patch_all()
|
||||||
|
|
||||||
|
def patch(self, method):
|
||||||
|
self.originals[method] = getattr(self.obj, method)
|
||||||
|
_m = patch.object(self.obj, method)
|
||||||
|
mock = _m.start()
|
||||||
|
self.addCleanup(_m.stop)
|
||||||
|
return mock
|
||||||
|
|
||||||
|
def patch_all(self):
|
||||||
|
for method in self.patches:
|
||||||
|
setattr(self, method, self.patch(method))
|
||||||
|
|
||||||
|
|
||||||
|
class TestConfig(object):
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.config = get_default_config()
|
||||||
|
self.config_prev = {}
|
||||||
|
|
||||||
|
def previous(self, k):
|
||||||
|
return self.config_prev[k] if k in self.config_prev else self.config[k]
|
||||||
|
|
||||||
|
def set_previous(self, k, v):
|
||||||
|
self.config_prev[k] = v
|
||||||
|
|
||||||
|
def unset_previous(self, k):
|
||||||
|
if k in self.config_prev:
|
||||||
|
self.config_prev.pop(k)
|
||||||
|
|
||||||
|
def get(self, attr=None):
|
||||||
|
if not attr:
|
||||||
|
return self
|
||||||
|
try:
|
||||||
|
return self.config[attr]
|
||||||
|
except KeyError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_all(self):
|
||||||
|
return self.config
|
||||||
|
|
||||||
|
def set(self, attr, value):
|
||||||
|
if attr not in self.config:
|
||||||
|
raise KeyError
|
||||||
|
self.config[attr] = value
|
||||||
|
|
||||||
|
def __getitem__(self, key):
|
||||||
|
return self.get(key)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRelation(object):
|
||||||
|
|
||||||
|
def __init__(self, relation_data={}):
|
||||||
|
self.relation_data = relation_data
|
||||||
|
|
||||||
|
def set(self, relation_data):
|
||||||
|
self.relation_data = relation_data
|
||||||
|
|
||||||
|
def get(self, attr=None, unit=None, rid=None):
|
||||||
|
if attr is None:
|
||||||
|
return self.relation_data
|
||||||
|
elif attr in self.relation_data:
|
||||||
|
return self.relation_data[attr]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def patch_open():
|
||||||
|
'''Patch open() to allow mocking both open() itself and the file that is
|
||||||
|
yielded.
|
||||||
|
|
||||||
|
Yields the mock for "open" and "file", respectively.'''
|
||||||
|
mock_open = MagicMock(spec=open)
|
||||||
|
mock_file = MagicMock(spec=file)
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def stub_open(*args, **kwargs):
|
||||||
|
mock_open(*args, **kwargs)
|
||||||
|
yield mock_file
|
||||||
|
|
||||||
|
with patch('__builtin__.open', stub_open):
|
||||||
|
yield mock_open, mock_file
|
Loading…
x
Reference in New Issue
Block a user