charm-percona-cluster/hooks/percona_utils.py

249 lines
7.8 KiB
Python

''' General utilities for percona '''
import subprocess
from subprocess import Popen, PIPE
import socket
import tempfile
import os
import shutil
from charmhelpers.core.host import (
lsb_release
)
from charmhelpers.core.hookenv import (
charm_dir,
unit_get,
relation_ids,
related_units,
relation_get,
relation_set,
local_unit,
service_name,
config,
log,
DEBUG,
)
from charmhelpers.fetch import (
apt_install,
filter_installed_packages
)
from charmhelpers.contrib.network.ip import (
get_ipv6_addr
)
from charmhelpers.contrib.database.mysql import (
MySQLHelper,
)
PACKAGES = [
'percona-xtradb-cluster-server-5.5',
'percona-xtradb-cluster-client-5.5',
]
KEY = "keys/repo.percona.com"
REPO = """deb http://repo.percona.com/apt {release} main
deb-src http://repo.percona.com/apt {release} main"""
MY_CNF = "/etc/mysql/my.cnf"
SEEDED_MARKER = "/var/lib/mysql/seeded"
HOSTS_FILE = '/etc/hosts'
def seeded():
''' Check whether service unit is already seeded '''
return os.path.exists(SEEDED_MARKER)
def mark_seeded():
''' Mark service unit as seeded '''
with open(SEEDED_MARKER, 'w') as seeded:
seeded.write('done')
def setup_percona_repo():
''' Configure service unit to use percona repositories '''
with open('/etc/apt/sources.list.d/percona.list', 'w') as sources:
sources.write(REPO.format(release=lsb_release()['DISTRIB_CODENAME']))
subprocess.check_call(['apt-key', 'add', KEY])
def get_host_ip(hostname=None):
try:
import dns.resolver
except ImportError:
apt_install(filter_installed_packages(['python-dnspython']),
fatal=True)
import dns.resolver
if config('prefer-ipv6'):
# Ensure we have a valid ipv6 address configured
get_ipv6_addr(exc_list=[config('vip')], fatal=True)[0]
return socket.gethostname()
hostname = hostname or unit_get('private-address')
try:
# Test to see if already an IPv4 address
socket.inet_aton(hostname)
return hostname
except socket.error:
# This may throw an NXDOMAIN exception; in which case
# things are badly broken so just let it kill the hook
answers = dns.resolver.query(hostname, 'A')
if answers:
return answers[0].address
def get_cluster_hosts():
hosts_map = {}
hostname = get_host_ip()
hosts = [hostname]
# We need to add this localhost dns name to /etc/hosts along with peer
# hosts to ensure percona gets consistently resolved addresses.
if config('prefer-ipv6'):
addr = get_ipv6_addr(exc_list=[config('vip')], fatal=True)[0]
hosts_map = {addr: hostname}
for relid in relation_ids('cluster'):
for unit in related_units(relid):
rdata = relation_get(unit=unit, rid=relid)
private_address = rdata.get('private-address')
if config('prefer-ipv6'):
hostname = rdata.get('hostname')
if not hostname or hostname in hosts:
log("(unit=%s) Ignoring hostname '%s' provided by cluster "
"relation for addr %s" %
(unit, hostname, private_address), level=DEBUG)
continue
else:
log("(unit=%s) hostname '%s' provided by cluster relation "
"for addr %s" % (unit, hostname, private_address),
level=DEBUG)
hosts_map[private_address] = hostname
hosts.append(hostname)
else:
hosts.append(get_host_ip(private_address))
if hosts_map:
update_hosts_file(hosts_map)
return hosts
SQL_SST_USER_SETUP = ("GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT ON *.* "
"TO 'sstuser'@'localhost' IDENTIFIED BY '{}'")
SQL_SST_USER_SETUP_IPV6 = ("GRANT RELOAD, LOCK TABLES, REPLICATION CLIENT "
"ON *.* TO 'sstuser'@'ip6-localhost' IDENTIFIED "
"BY '{}'")
def get_db_helper():
return MySQLHelper(rpasswdf_template='/var/lib/charm/%s/mysql.passwd' %
(service_name()),
upasswdf_template='/var/lib/charm/%s/mysql-{}.passwd' %
(service_name()))
def configure_sstuser(sst_password):
m_helper = get_db_helper()
m_helper.connect(password=m_helper.get_mysql_root_password())
m_helper.execute(SQL_SST_USER_SETUP.format(sst_password))
m_helper.execute(SQL_SST_USER_SETUP_IPV6.format(sst_password))
# TODO: mysql charmhelper
def configure_mysql_root_password(password):
''' Configure debconf with root password '''
dconf = Popen(['debconf-set-selections'], stdin=PIPE)
# Set both percona and mysql password options to cover
# both upstream and distro packages.
packages = ["percona-server-server", "mysql-server"]
m_helper = get_db_helper()
root_pass = m_helper.get_mysql_root_password(password)
for package in packages:
dconf.stdin.write("%s %s/root_password password %s\n" %
(package, package, root_pass))
dconf.stdin.write("%s %s/root_password_again password %s\n" %
(package, package, root_pass))
dconf.communicate()
dconf.wait()
# TODO: Submit for charmhelper
def relation_clear(r_id=None):
''' Clears any relation data already set on relation r_id '''
settings = relation_get(rid=r_id,
unit=local_unit())
for setting in settings:
if setting not in ['public-address', 'private-address']:
settings[setting] = None
relation_set(relation_id=r_id,
**settings)
def update_hosts_file(map):
"""Percona does not currently like ipv6 addresses so we need to use dns
names instead. In order to make them resolvable we ensure they are in
/etc/hosts.
See https://bugs.launchpad.net/galera/+bug/1130595 for some more info.
"""
with open(HOSTS_FILE, 'r') as hosts:
lines = hosts.readlines()
log("Updating %s with: %s (current: %s)" % (HOSTS_FILE, map, lines),
level=DEBUG)
newlines = []
for ip, hostname in map.items():
if not ip or not hostname:
continue
keepers = []
for line in lines:
_line = line.split()
if len(line) < 2 or not (_line[0] == ip or hostname in _line[1:]):
keepers.append(line)
else:
log("Marking line '%s' for update or removal" % (line.strip()),
level=DEBUG)
lines = keepers
newlines.append("%s %s\n" % (ip, hostname))
lines += newlines
with tempfile.NamedTemporaryFile(delete=False) as tmpfile:
with open(tmpfile.name, 'w') as hosts:
for line in lines:
hosts.write(line)
os.rename(tmpfile.name, HOSTS_FILE)
os.chmod(HOSTS_FILE, 0o644)
def assert_charm_supports_ipv6():
"""Check whether we are able to support charms ipv6."""
if lsb_release()['DISTRIB_CODENAME'].lower() < "trusty":
raise Exception("IPv6 is not supported in the charms for Ubuntu "
"versions less than Trusty 14.04")
def unit_sorted(units):
"""Return a sorted list of unit names."""
return sorted(
units, lambda a, b: cmp(int(a.split('/')[-1]), int(b.split('/')[-1])))
def install_mysql_ocf():
dest_dir = '/usr/lib/ocf/resource.d/percona/'
for fname in ['ocf/percona/mysql_monitor']:
src_file = os.path.join(charm_dir(), fname)
if not os.path.isdir(dest_dir):
os.makedirs(dest_dir)
dest_file = os.path.join(dest_dir, os.path.basename(src_file))
if not os.path.exists(dest_file):
log('Installing %s' % dest_file, level='INFO')
shutil.copy(src_file, dest_file)
else:
log("'%s' already exists, skipping" % dest_file, level='INFO')