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:
parent
9e632ba1d6
commit
f756931899
|
@ -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__":
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
../charmhelpers
|
|
@ -1 +0,0 @@
|
|||
../hooks/percona_utils.py
|
|
@ -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=*
|
||||
|
|
|
@ -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
|
@ -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/>.
|
|
@ -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])
|
|
@ -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/>.
|
|
@ -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
|
@ -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, "")
|
|
@ -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])
|
|
@ -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))
|
|
@ -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):
|
||||
|
|
|
@ -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__':
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
|
@ -1,3 +1,4 @@
|
|||
import sys
|
||||
|
||||
sys.path.append('hooks')
|
||||
sys.path.append('actions')
|
||||
|
|
|
@ -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"])
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue