Enhanced pause/resume for maintenance mode

Implemented pause/resume with checking whether the mysqld process
is running on the unit when paused.
Synced charm-helpers and modified charm-helpers-hooks.yaml to pull in
the relevant pause-helper modules plus ancillary stuff that
charmhelpers.contrib.openstack.utils depends on.

Change-Id: I510b2566bd44342cc19491b380186af0c87e080a
This commit is contained in:
Alex Kavanagh 2016-04-05 14:43:17 +00:00
parent 9e632ba1d6
commit f756931899
21 changed files with 3592 additions and 62 deletions

View File

@ -6,6 +6,8 @@ import subprocess
import traceback
from time import gmtime, strftime
sys.path.append('hooks')
from charmhelpers.core.host import service_pause, service_resume
from charmhelpers.core.hookenv import (
action_get,
@ -15,9 +17,12 @@ from charmhelpers.core.hookenv import (
config,
)
from percona_utils import assess_status
MYSQL_SERVICE = "mysql"
from percona_utils import (
pause_unit_helper,
resume_unit_helper,
register_configs,
)
from percona_hooks import config_changed
def pause(args):
@ -25,20 +30,18 @@ def pause(args):
@raises Exception should the service fail to stop.
"""
if not service_pause(MYSQL_SERVICE):
raise Exception("Failed to pause MySQL service.")
status_set(
"maintenance",
"Unit paused - use 'resume' action to resume normal service")
pause_unit_helper(register_configs())
def resume(args):
"""Resume the MySQL service.
@raises Exception should the service fail to start."""
if not service_resume(MYSQL_SERVICE):
raise Exception("Failed to resume MySQL service.")
assess_status()
@raises Exception should the service fail to start.
"""
resume_unit_helper(register_configs())
# NOTE(ajkavanagh) - we force a config_changed pseudo-hook to see if the
# unit needs to bootstrap or restart it's services here.
config_changed()
def backup():
@ -87,12 +90,14 @@ def main(args):
try:
action = ACTIONS[action_name]
except KeyError:
return "Action %s undefined" % action_name
s = "Action {} undefined".format(action_name)
action_fail(s)
return s
else:
try:
action(args)
except Exception as e:
action_fail(str(e))
action_fail("Action {} failed: {}".format(action_name, str(e)))
if __name__ == "__main__":

View File

@ -1 +0,0 @@
../charmhelpers

View File

@ -1 +0,0 @@
../hooks/percona_utils.py

View File

@ -5,9 +5,12 @@ include:
- cli
- fetch
- contrib.hahelpers.cluster
- contrib.openstack.utils
- contrib.storage.linux
- contrib.python.packages
- contrib.peerstorage
- payload.execd
- contrib.network.ip
- contrib.database
- contrib.charmsupport
- contrib.hardening|inc=*
- contrib.hardening|inc=*

View File

@ -0,0 +1,15 @@
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.

View File

@ -0,0 +1,145 @@
#!/usr/bin/env python
# coding: utf-8
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import os
import subprocess
import sys
from charmhelpers.fetch import apt_install, apt_update
from charmhelpers.core.hookenv import charm_dir, log
__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
def pip_execute(*args, **kwargs):
"""Overriden pip_execute() to stop sys.path being changed.
The act of importing main from the pip module seems to cause add wheels
from the /usr/share/python-wheels which are installed by various tools.
This function ensures that sys.path remains the same after the call is
executed.
"""
try:
_path = sys.path
try:
from pip import main as _pip_execute
except ImportError:
apt_update()
apt_install('python-pip')
from pip import main as _pip_execute
_pip_execute(*args, **kwargs)
finally:
sys.path = _path
def parse_options(given, available):
"""Given a set of options, check if available"""
for key, value in sorted(given.items()):
if not value:
continue
if key in available:
yield "--{0}={1}".format(key, value)
def pip_install_requirements(requirements, constraints=None, **options):
"""Install a requirements file.
:param constraints: Path to pip constraints file.
http://pip.readthedocs.org/en/stable/user_guide/#constraints-files
"""
command = ["install"]
available_options = ('proxy', 'src', 'log', )
for option in parse_options(options, available_options):
command.append(option)
command.append("-r {0}".format(requirements))
if constraints:
command.append("-c {0}".format(constraints))
log("Installing from file: {} with constraints {} "
"and options: {}".format(requirements, constraints, command))
else:
log("Installing from file: {} with options: {}".format(requirements,
command))
pip_execute(command)
def pip_install(package, fatal=False, upgrade=False, venv=None, **options):
"""Install a python package"""
if venv:
venv_python = os.path.join(venv, 'bin/pip')
command = [venv_python, "install"]
else:
command = ["install"]
available_options = ('proxy', 'src', 'log', 'index-url', )
for option in parse_options(options, available_options):
command.append(option)
if upgrade:
command.append('--upgrade')
if isinstance(package, list):
command.extend(package)
else:
command.append(package)
log("Installing {} package with options: {}".format(package,
command))
if venv:
subprocess.check_call(command)
else:
pip_execute(command)
def pip_uninstall(package, **options):
"""Uninstall a python package"""
command = ["uninstall", "-q", "-y"]
available_options = ('proxy', 'log', )
for option in parse_options(options, available_options):
command.append(option)
if isinstance(package, list):
command.extend(package)
else:
command.append(package)
log("Uninstalling {} package with options: {}".format(package,
command))
pip_execute(command)
def pip_list():
"""Returns the list of current python installed packages
"""
return pip_execute(["list"])
def pip_create_virtualenv(path=None):
"""Create an isolated Python environment."""
apt_install('python-virtualenv')
if path:
venv_path = path
else:
venv_path = os.path.join(charm_dir(), 'venv')
if not os.path.exists(venv_path):
subprocess.check_call(['virtualenv', venv_path])

View File

@ -0,0 +1,15 @@
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.

View File

@ -0,0 +1,15 @@
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,88 @@
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import os
import re
from subprocess import (
check_call,
check_output,
)
import six
##################################################
# loopback device helpers.
##################################################
def loopback_devices():
'''
Parse through 'losetup -a' output to determine currently mapped
loopback devices. Output is expected to look like:
/dev/loop0: [0807]:961814 (/tmp/my.img)
:returns: dict: a dict mapping {loopback_dev: backing_file}
'''
loopbacks = {}
cmd = ['losetup', '-a']
devs = [d.strip().split(' ') for d in
check_output(cmd).splitlines() if d != '']
for dev, _, f in devs:
loopbacks[dev.replace(':', '')] = re.search('\((\S+)\)', f).groups()[0]
return loopbacks
def create_loopback(file_path):
'''
Create a loopback device for a given backing file.
:returns: str: Full path to new loopback device (eg, /dev/loop0)
'''
file_path = os.path.abspath(file_path)
check_call(['losetup', '--find', file_path])
for d, f in six.iteritems(loopback_devices()):
if f == file_path:
return d
def ensure_loopback_device(path, size):
'''
Ensure a loopback device exists for a given backing file path and size.
If it a loopback device is not mapped to file, a new one will be created.
TODO: Confirm size of found loopback device.
:returns: str: Full path to the ensured loopback device (eg, /dev/loop0)
'''
for d, f in six.iteritems(loopback_devices()):
if f == path:
return d
if not os.path.exists(path):
cmd = ['truncate', '--size', size, path]
check_call(cmd)
return create_loopback(path)
def is_mapped_loopback_device(device):
"""
Checks if a given device name is an existing/mapped loopback device.
:param device: str: Full path to the device (eg, /dev/loop1).
:returns: str: Path to the backing file if is a loopback device
empty string otherwise
"""
return loopback_devices().get(device, "")

View File

@ -0,0 +1,105 @@
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
from subprocess import (
CalledProcessError,
check_call,
check_output,
Popen,
PIPE,
)
##################################################
# LVM helpers.
##################################################
def deactivate_lvm_volume_group(block_device):
'''
Deactivate any volume gruop associated with an LVM physical volume.
:param block_device: str: Full path to LVM physical volume
'''
vg = list_lvm_volume_group(block_device)
if vg:
cmd = ['vgchange', '-an', vg]
check_call(cmd)
def is_lvm_physical_volume(block_device):
'''
Determine whether a block device is initialized as an LVM PV.
:param block_device: str: Full path of block device to inspect.
:returns: boolean: True if block device is a PV, False if not.
'''
try:
check_output(['pvdisplay', block_device])
return True
except CalledProcessError:
return False
def remove_lvm_physical_volume(block_device):
'''
Remove LVM PV signatures from a given block device.
:param block_device: str: Full path of block device to scrub.
'''
p = Popen(['pvremove', '-ff', block_device],
stdin=PIPE)
p.communicate(input='y\n')
def list_lvm_volume_group(block_device):
'''
List LVM volume group associated with a given block device.
Assumes block device is a valid LVM PV.
:param block_device: str: Full path of block device to inspect.
:returns: str: Name of volume group associated with block device or None
'''
vg = None
pvd = check_output(['pvdisplay', block_device]).splitlines()
for l in pvd:
l = l.decode('UTF-8')
if l.strip().startswith('VG Name'):
vg = ' '.join(l.strip().split()[2:])
return vg
def create_lvm_physical_volume(block_device):
'''
Initialize a block device as an LVM physical volume.
:param block_device: str: Full path of block device to initialize.
'''
check_call(['pvcreate', block_device])
def create_lvm_volume_group(volume_group, block_device):
'''
Create an LVM volume group backed by a given block device.
Assumes block device has already been initialized as an LVM PV.
:param volume_group: str: Name of volume group to create.
:block_device: str: Full path of PV-initialized block device.
'''
check_call(['vgcreate', volume_group, block_device])

View File

@ -0,0 +1,71 @@
# Copyright 2014-2015 Canonical Limited.
#
# This file is part of charm-helpers.
#
# charm-helpers is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3 as
# published by the Free Software Foundation.
#
# charm-helpers is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import os
import re
from stat import S_ISBLK
from subprocess import (
check_call,
check_output,
call
)
def is_block_device(path):
'''
Confirm device at path is a valid block device node.
:returns: boolean: True if path is a block device, False if not.
'''
if not os.path.exists(path):
return False
return S_ISBLK(os.stat(path).st_mode)
def zap_disk(block_device):
'''
Clear a block device of partition table. Relies on sgdisk, which is
installed as pat of the 'gdisk' package in Ubuntu.
:param block_device: str: Full path of block device to clean.
'''
# https://github.com/ceph/ceph/commit/fdd7f8d83afa25c4e09aaedd90ab93f3b64a677b
# sometimes sgdisk exits non-zero; this is OK, dd will clean up
call(['sgdisk', '--zap-all', '--', block_device])
call(['sgdisk', '--clear', '--mbrtogpt', '--', block_device])
dev_end = check_output(['blockdev', '--getsz',
block_device]).decode('UTF-8')
gpt_end = int(dev_end.split()[0]) - 100
check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),
'bs=1M', 'count=1'])
check_call(['dd', 'if=/dev/zero', 'of=%s' % (block_device),
'bs=512', 'count=100', 'seek=%s' % (gpt_end)])
def is_device_mounted(device):
'''Given a device path, return True if that device is mounted, and False
if it isn't.
:param device: str: Full path of the device to check.
:returns: boolean: True if the path represents a mounted device, False if
it doesn't.
'''
is_partition = bool(re.search(r".*[0-9]+\b", device))
out = check_output(['mount']).decode('UTF-8')
if is_partition:
return bool(re.search(device + r"\b", out))
return bool(re.search(device + r"[0-9]*\b", out))

View File

@ -128,6 +128,13 @@ def service(action, service_name):
return subprocess.call(cmd) == 0
def systemv_services_running():
output = subprocess.check_output(
['service', '--status-all'],
stderr=subprocess.STDOUT).decode('UTF-8')
return [row.split()[-1] for row in output.split('\n') if '[ + ]' in row]
def service_running(service_name):
"""Determine whether a system service is running"""
if init_is_systemd():
@ -140,11 +147,15 @@ def service_running(service_name):
except subprocess.CalledProcessError:
return False
else:
# This works for upstart scripts where the 'service' command
# returns a consistent string to represent running 'start/running'
if ("start/running" in output or "is running" in output or
"up and running" in output):
return True
else:
return False
# Check System V scripts init script return codes
if service_name in systemv_services_running():
return True
return False
def service_available(service_name):

View File

@ -44,26 +44,6 @@ from charmhelpers.contrib.peerstorage import (
peer_store_and_set,
peer_retrieve_by_prefix,
)
from percona_utils import (
determine_packages,
setup_percona_repo,
get_host_ip,
get_cluster_hosts,
configure_sstuser,
configure_mysql_root_password,
relation_clear,
assert_charm_supports_ipv6,
unit_sorted,
get_db_helper,
mark_seeded, seeded,
install_mysql_ocf,
is_sufficient_peers,
notify_bootstrapped,
is_bootstrapped,
get_wsrep_value,
assess_status,
resolve_cnf_file,
)
from charmhelpers.contrib.database.mysql import (
PerconaClusterHelper,
)
@ -83,11 +63,34 @@ from charmhelpers.contrib.network.ip import (
is_address_in_network,
resolve_network_cidr,
)
from charmhelpers.contrib.charmsupport import nrpe
from charmhelpers.contrib.hardening.harden import harden
from charmhelpers.contrib.hardening.mysql.checks import run_mysql_checks
from charmhelpers.contrib.openstack.utils import (
is_unit_paused_set,
)
from percona_utils import (
determine_packages,
setup_percona_repo,
get_host_ip,
get_cluster_hosts,
configure_sstuser,
configure_mysql_root_password,
relation_clear,
assert_charm_supports_ipv6,
unit_sorted,
get_db_helper,
mark_seeded, seeded,
install_mysql_ocf,
is_sufficient_peers,
notify_bootstrapped,
is_bootstrapped,
get_wsrep_value,
assess_status,
register_configs,
resolve_cnf_file,
)
hooks = Hooks()
@ -241,6 +244,11 @@ def upgrade():
@hooks.hook('config-changed')
@harden()
def config_changed():
# if we are paused, delay doing any config changed hooks. It is forced on
# the resume.
if is_unit_paused_set():
return
if config('prefer-ipv6'):
assert_charm_supports_ipv6()
@ -671,7 +679,7 @@ def main():
hooks.execute(sys.argv)
except UnregisteredHookError as e:
log('Unknown hook {} - skipping.'.format(e))
assess_status()
assess_status(register_configs())
if __name__ == '__main__':

View File

@ -25,7 +25,6 @@ from charmhelpers.core.hookenv import (
INFO,
WARNING,
ERROR,
status_set,
cached,
)
from charmhelpers.fetch import (
@ -38,6 +37,11 @@ from charmhelpers.contrib.network.ip import (
from charmhelpers.contrib.database.mysql import (
MySQLHelper,
)
from charmhelpers.contrib.openstack.utils import (
make_assess_status_func,
pause_unit,
resume_unit,
)
# NOTE: python-mysqldb is installed by charmhelpers.contrib.database.mysql so
# hence why we import here
@ -50,6 +54,10 @@ REPO = """deb http://repo.percona.com/apt {release} main
deb-src http://repo.percona.com/apt {release} main"""
SEEDED_MARKER = "{data_dir}/seeded"
HOSTS_FILE = '/etc/hosts'
# NOTE(ajkavanagh) - this is 'required' for the pause/resume code for
# maintenance mode, but is currently not populated as the
# charm_check_function() checks whether the unit is working properly.
REQUIRED_INTERFACES = {}
def determine_packages():
@ -376,25 +384,27 @@ def cluster_in_sync():
return False
def assess_status():
'''Assess the status of the current unit'''
def charm_check_func():
"""Custom function to assess the status of the current unit
@returns (status, message) - tuple of strings if an issue
"""
min_size = config('min-cluster-size')
# Ensure that number of peers > cluster size configuration
if not is_sufficient_peers():
status_set('blocked', 'Insufficient peers to bootstrap cluster')
return
return ('blocked', 'Insufficient peers to bootstrap cluster')
if min_size and int(min_size) > 1:
# Once running, ensure that cluster is in sync
# and has the required peers
if not is_bootstrapped():
status_set('waiting', 'Unit waiting for cluster bootstrap')
return ('waiting', 'Unit waiting for cluster bootstrap')
elif is_bootstrapped() and cluster_in_sync():
status_set('active', 'Unit is ready and clustered')
return ('active', 'Unit is ready and clustered')
else:
status_set('blocked', 'Unit is not in sync')
return ('blocked', 'Unit is not in sync')
else:
status_set('active', 'Unit is ready')
return ('active', 'Unit is ready')
@cached
@ -411,3 +421,100 @@ def resolve_cnf_file():
return '/etc/mysql/my.cnf'
else:
return '/etc/mysql/percona-xtradb-cluster.conf.d/mysqld.cnf'
class FakeOSConfigRenderer(object):
"""This class is to provide to register_configs() as a 'fake'
OSConfigRenderer object that has a complete_contexts method that returns
an empty list. This is so that the pause/resume framework can be used
from charmhelpers that requires configs to be able to run.
This is a bit of a hack, but via Python's duck-typing enables the function
to work.
"""
def complete_contexts(self):
return []
def register_configs():
"""Return a OSConfigRenderer object.
However, ceph-mon wasn't written using OSConfigRenderer objects to do the
config files, so this just returns an empty OSConfigRenderer object.
@returns empty FakeOSConfigRenderer object.
"""
return FakeOSConfigRenderer()
def services():
"""Return a list of services that are managed by this charm.
@returns [services] - list of strings that are service names.
"""
return ['mysql']
def assess_status(configs):
"""Assess status of current unit
Decides what the state of the unit should be based on the current
configuration.
SIDE EFFECT: calls set_os_workload_status(...) which sets the workload
status of the unit.
Also calls status_set(...) directly if paused state isn't complete.
@param configs: a templating.OSConfigRenderer() object
@returns None - this function is executed for its side-effect
"""
assess_status_func(configs)()
def assess_status_func(configs):
"""Helper function to create the function that will assess_status() for
the unit.
Uses charmhelpers.contrib.openstack.utils.make_assess_status_func() to
create the appropriate status function and then returns it.
Used directly by assess_status() and also for pausing and resuming
the unit.
NOTE(ajkavanagh) ports are not checked due to race hazards with services
that don't behave sychronously w.r.t their service scripts. e.g.
apache2.
@param configs: a templating.OSConfigRenderer() object
@return f() -> None : a function that assesses the unit's workload status
"""
return make_assess_status_func(
configs, REQUIRED_INTERFACES,
charm_func=lambda _: charm_check_func(),
services=services(), ports=None)
def pause_unit_helper(configs):
"""Helper function to pause a unit, and then call assess_status(...) in
effect, so that the status is correctly updated.
Uses charmhelpers.contrib.openstack.utils.pause_unit() to do the work.
@param configs: a templating.OSConfigRenderer() object
@returns None - this function is executed for its side-effect
"""
_pause_resume_helper(pause_unit, configs)
def resume_unit_helper(configs):
"""Helper function to resume a unit, and then call assess_status(...) in
effect, so that the status is correctly updated.
Uses charmhelpers.contrib.openstack.utils.resume_unit() to do the work.
@param configs: a templating.OSConfigRenderer() object
@returns None - this function is executed for its side-effect
"""
_pause_resume_helper(resume_unit, configs)
def _pause_resume_helper(f, configs):
"""Helper function that uses the make_assess_status_func(...) from
charmhelpers.contrib.openstack.utils to create an assess_status(...)
function that can be used with the pause/resume of the unit
@param f: the function to be used with the assess_status(...) function
@returns None - this function is executed for its side-effect
"""
# TODO(ajkavanagh) - ports= has been left off because of the race hazard
# that exists due to service_start()
f(assess_status_func(configs),
services=services(),
ports=None)

View File

@ -0,0 +1,39 @@
#!/usr/bin/env python
# test percona-cluster (1 node) with pause and resume.
from charmhelpers.contrib.openstack.amulet.utils import ( # noqa
OpenStackAmuletUtils,
DEBUG,
# ERROR
)
import basic_deployment
u = OpenStackAmuletUtils(DEBUG)
class SingleNode(basic_deployment.BasicDeployment):
def __init__(self):
super(SingleNode, self).__init__(units=1)
def run(self):
super(SingleNode, self).run()
assert self.is_pxc_bootstrapped(), "Cluster not bootstrapped"
sentry_unit = self.d.sentry.unit['percona-cluster/0']
assert u.status_get(sentry_unit)[0] == "active"
action_id = u.run_action(sentry_unit, "pause")
assert u.wait_on_action(action_id), "Pause action failed."
assert u.status_get(sentry_unit)[0] == "maintenance"
action_id = u.run_action(sentry_unit, "resume")
assert u.wait_on_action(action_id), "Resume action failed."
assert u.status_get(sentry_unit)[0] == "active"
u.log.debug('OK')
if __name__ == "__main__":
t = SingleNode()
t.run()

View File

@ -1,3 +1,4 @@
import sys
sys.path.append('hooks')
sys.path.append('actions')

View File

@ -0,0 +1,79 @@
import sys
import mock
from mock import patch
from test_utils import CharmTestCase
# python-apt is not installed as part of test-requirements but is imported by
# some charmhelpers modules so create a fake import.
sys.modules['apt'] = mock.MagicMock()
sys.modules['MySQLdb'] = mock.MagicMock()
# we have to patch out harden decorator because hooks/percona_hooks.py gets
# imported via actions.py and will freak out if it trys to run in the context
# of a test.
with patch('percona_utils.register_configs') as configs, \
patch('charmhelpers.contrib.hardening.harden.harden') as mock_dec:
mock_dec.side_effect = (lambda *dargs, **dkwargs: lambda f:
lambda *args, **kwargs: f(*args, **kwargs))
configs.return_value = 'test-config'
import actions
class PauseTestCase(CharmTestCase):
def setUp(self):
super(PauseTestCase, self).setUp(
actions, ["pause_unit_helper"])
def test_pauses_services(self):
actions.pause([])
self.pause_unit_helper.assert_called_once_with('test-config')
class ResumeTestCase(CharmTestCase):
def setUp(self):
super(ResumeTestCase, self).setUp(
actions, ["resume_unit_helper"])
def test_pauses_services(self):
with patch('actions.config_changed') as config_changed:
actions.resume([])
self.resume_unit_helper.assert_called_once_with('test-config')
config_changed.assert_called_once_with()
class MainTestCase(CharmTestCase):
def setUp(self):
super(MainTestCase, self).setUp(actions, ["action_fail"])
def test_invokes_action(self):
dummy_calls = []
def dummy_action(args):
dummy_calls.append(True)
with mock.patch.dict(actions.ACTIONS, {"foo": dummy_action}):
actions.main(["foo"])
self.assertEqual(dummy_calls, [True])
def test_unknown_action(self):
"""Unknown actions aren't a traceback."""
exit_string = actions.main(["foo"])
self.assertEqual("Action foo undefined", exit_string)
def test_failing_action(self):
"""Actions which traceback trigger action_fail() calls."""
dummy_calls = []
self.action_fail.side_effect = dummy_calls.append
def dummy_action(args):
raise ValueError("uh oh")
with mock.patch.dict(actions.ACTIONS, {"foo": dummy_action}):
actions.main(["foo"])
self.assertEqual(dummy_calls, ["Action foo failed: uh oh"])

View File

@ -186,7 +186,7 @@ class UtilsTests(unittest.TestCase):
TO_PATCH = [
'status_set',
# 'status_set',
'is_sufficient_peers',
'is_bootstrapped',
'config',
@ -201,34 +201,77 @@ class TestAssessStatus(CharmTestCase):
def test_single_unit(self):
self.config.return_value = None
self.is_sufficient_peers.return_value = True
percona_utils.assess_status()
self.status_set.assert_called_with('active', mock.ANY)
stat, _ = percona_utils.charm_check_func()
assert stat == 'active'
def test_insufficient_peers(self):
self.config.return_value = 3
self.is_sufficient_peers.return_value = False
percona_utils.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
stat, _ = percona_utils.charm_check_func()
assert stat == 'blocked'
def test_not_bootstrapped(self):
self.config.return_value = 3
self.is_sufficient_peers.return_value = True
self.is_bootstrapped.return_value = False
percona_utils.assess_status()
self.status_set.assert_called_with('waiting', mock.ANY)
stat, _ = percona_utils.charm_check_func()
assert stat == 'waiting'
def test_bootstrapped_in_sync(self):
self.config.return_value = 3
self.is_sufficient_peers.return_value = True
self.is_bootstrapped.return_value = True
self.cluster_in_sync.return_value = True
percona_utils.assess_status()
self.status_set.assert_called_with('active', mock.ANY)
stat, _ = percona_utils.charm_check_func()
assert stat == 'active'
def test_bootstrapped_not_in_sync(self):
self.config.return_value = 3
self.is_sufficient_peers.return_value = True
self.is_bootstrapped.return_value = True
self.cluster_in_sync.return_value = False
percona_utils.assess_status()
self.status_set.assert_called_with('blocked', mock.ANY)
stat, _ = percona_utils.charm_check_func()
assert stat == 'blocked'
def test_assess_status(self):
with mock.patch.object(percona_utils, 'assess_status_func') as asf:
callee = mock.MagicMock()
asf.return_value = callee
percona_utils.assess_status('test-config')
asf.assert_called_once_with('test-config')
callee.assert_called_once_with()
@mock.patch.object(percona_utils, 'REQUIRED_INTERFACES')
@mock.patch.object(percona_utils, 'services')
@mock.patch.object(percona_utils, 'make_assess_status_func')
def test_assess_status_func(self,
make_assess_status_func,
services,
REQUIRED_INTERFACES):
services.return_value = 's1'
percona_utils.assess_status_func('test-config')
# ports=None whilst port checks are disabled.
make_assess_status_func.assert_called_once_with(
'test-config', REQUIRED_INTERFACES, charm_func=mock.ANY,
services='s1', ports=None)
def test_pause_unit_helper(self):
with mock.patch.object(percona_utils, '_pause_resume_helper') as prh:
percona_utils.pause_unit_helper('random-config')
prh.assert_called_once_with(percona_utils.pause_unit,
'random-config')
with mock.patch.object(percona_utils, '_pause_resume_helper') as prh:
percona_utils.resume_unit_helper('random-config')
prh.assert_called_once_with(percona_utils.resume_unit,
'random-config')
@mock.patch.object(percona_utils, 'services')
def test_pause_resume_helper(self, services):
f = mock.MagicMock()
services.return_value = 's1'
with mock.patch.object(percona_utils, 'assess_status_func') as asf:
asf.return_value = 'assessor'
percona_utils._pause_resume_helper(f, 'some-config')
asf.assert_called_once_with('some-config')
# ports=None whilst port checks are disabled.
f.assert_called_once_with('assessor', services='s1', ports=None)